@transloadit/utils 4.7.0 → 4.7.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 +18 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/node.d.ts +6 -31
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +18 -98
- package/dist/smartCdn.d.ts +3 -2
- package/dist/smartCdn.d.ts.map +1 -1
- package/dist/smartCdn.js +23 -8
- package/dist/smartCdnImage.d.ts +68 -0
- package/dist/smartCdnImage.d.ts.map +1 -0
- package/dist/smartCdnImage.js +137 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,6 +55,9 @@ const { workspace, template, input, urlParams, auth } = parseSmartCdnUrl(url)
|
|
|
55
55
|
const unsigned = stripSmartCdnAuth(url)
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
`auth_key`, `exp`, and `sig` are reserved: signed builders replace them and the unsigned builder
|
|
59
|
+
omits them. Other fields, including `hsh`, round-trip through the builders and parser.
|
|
60
|
+
|
|
58
61
|
Both builders accept a `baseUrl` that replaces `https://{workspace}.tlcdn.com`, for example a local
|
|
59
62
|
api2's URL Transform endpoint `https://api2-devdock.transloadit.dev/file/{workspace}` (a literal
|
|
60
63
|
`{workspace}` is substituted). The signature does not cover the host, so treat `baseUrl` as trusted
|
|
@@ -83,7 +86,12 @@ const imageCandidates = getSignedSmartCdnImageCandidates({
|
|
|
83
86
|
authSecret,
|
|
84
87
|
// Reuse one absolute expiry across a build instead of recomputing it per request.
|
|
85
88
|
expiresAt,
|
|
86
|
-
input
|
|
89
|
+
// The browser fallback is independent from the Template's input grammar.
|
|
90
|
+
fallbackUrl: '/images/photo.jpg',
|
|
91
|
+
// This workspace Template pins https://example.com/ and accepts a relative path.
|
|
92
|
+
input: 'images/photo.jpg',
|
|
93
|
+
sourceDimensions: { height: 1600, width: 2400 },
|
|
94
|
+
template: 'website-images',
|
|
87
95
|
widths: [320, 640, 960],
|
|
88
96
|
workspace,
|
|
89
97
|
})
|
|
@@ -108,4 +116,12 @@ for (const source of imageCandidates.sources) {
|
|
|
108
116
|
- `signParamsSync(paramsString, authSecret, algorithm?)`: Node-only sync signature helper.
|
|
109
117
|
- `getSignedSmartCdnUrl(options)` from `@transloadit/utils/node`: synchronous Smart CDN URL signer.
|
|
110
118
|
- `getSignedSmartCdnImageCandidates(options)`: deterministic structured, signed AVIF and WebP
|
|
111
|
-
candidates plus
|
|
119
|
+
candidates plus an explicit browser fallback. `template` is mandatory: use a trusted workspace
|
|
120
|
+
Template that owns its source policy. `fallbackUrl` is deliberately separate from `input`, which
|
|
121
|
+
may use a Template-specific grammar such as a relative origin-pinned path. Supply
|
|
122
|
+
`sourceDimensions` to prevent upscaling and keep both output dimensions within backend limits.
|
|
123
|
+
- `createSmartCdnImageCandidates(options, sign)` from `@transloadit/utils`: the same deterministic
|
|
124
|
+
image policy with an injected synchronous signer, for framework and package adapters that own
|
|
125
|
+
their credential boundary.
|
|
126
|
+
- `resolveSmartCdnImageFormats(formats)` and `resolveSmartCdnImageWidths(widths, maximumWidth?)`:
|
|
127
|
+
shared validation and normalization for adapters that use a different image Built-in.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { SmartCdnUrlOptions } from './smartCdn.ts';
|
|
2
2
|
export type SignatureAlgorithm = 'sha1' | 'sha256' | 'sha384' | 'sha512';
|
|
3
3
|
export type { ParsedSmartCdnUrl, ParseSmartCdnUrlOptions, SmartCdnUnsignedUrlOptions, SmartCdnUrlOptions, SmartCdnUrlParams, } from './smartCdn.ts';
|
|
4
|
+
export type { SignSmartCdnImageRequest, SmartCdnImageCandidate, SmartCdnImageCandidates, SmartCdnImageFormat, SmartCdnImageFormatQuality, SmartCdnImageFormats, SmartCdnImagePolicyOptions, SmartCdnImageSignRequest, SmartCdnImageSource, SmartCdnImageSourceDimensions, } from './smartCdnImage.ts';
|
|
4
5
|
export * from './assemblyInstructionsCompiler.ts';
|
|
5
6
|
export { getSmartCdnUrl, parseSmartCdnUrl, stripSmartCdnAuth } from './smartCdn.ts';
|
|
7
|
+
export { createSmartCdnImageCandidates, resolveSmartCdnImageFormats, resolveSmartCdnImageWidths, smartCdnImageMaxDimension, } from './smartCdnImage.ts';
|
|
6
8
|
export declare const signParams: (paramsString: string, authSecret: string, algorithm?: SignatureAlgorithm) => Promise<string>;
|
|
7
9
|
export type VerifyWebhookSignatureOptions = {
|
|
8
10
|
rawBody: string;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAIvD,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAExE,YAAY,EACV,iBAAiB,EACjB,uBAAuB,EACvB,0BAA0B,EAC1B,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAIvD,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAExE,YAAY,EACV,iBAAiB,EACjB,uBAAuB,EACvB,0BAA0B,EAC1B,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,eAAe,CAAA;AACtB,YAAY,EACV,wBAAwB,EACxB,sBAAsB,EACtB,uBAAuB,EACvB,mBAAmB,EACnB,0BAA0B,EAC1B,oBAAoB,EACpB,0BAA0B,EAC1B,wBAAwB,EACxB,mBAAmB,EACnB,6BAA6B,GAC9B,MAAM,oBAAoB,CAAA;AAE3B,cAAc,mCAAmC,CAAA;AACjD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACnF,OAAO,EACL,6BAA6B,EAC7B,2BAA2B,EAC3B,0BAA0B,EAC1B,yBAAyB,GAC1B,MAAM,oBAAoB,CAAA;AAqD3B,eAAO,MAAM,UAAU,GACrB,cAAc,MAAM,EACpB,YAAY,MAAM,EAClB,YAAW,kBAA6B,KACvC,OAAO,CAAC,MAAM,CAOhB,CAAA;AAED,MAAM,MAAM,6BAA6B,GAAG;IAC1C,OAAO,EAAE,MAAM,CAAA;IACf,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,eAAO,MAAM,sBAAsB,GACjC,SAAS,6BAA6B,KACrC,OAAO,CAAC,OAAO,CAkBjB,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,oBAAoB,GAAU,MAAM,kBAAkB,KAAG,OAAO,CAAC,MAAM,CAInF,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { finishSmartCdnUrl, prepareSmartCdnUrl } from "./smartCdn.js";
|
|
2
2
|
export * from "./assemblyInstructionsCompiler.js";
|
|
3
3
|
export { getSmartCdnUrl, parseSmartCdnUrl, stripSmartCdnAuth } from "./smartCdn.js";
|
|
4
|
+
export { createSmartCdnImageCandidates, resolveSmartCdnImageFormats, resolveSmartCdnImageWidths, smartCdnImageMaxDimension, } from "./smartCdnImage.js";
|
|
4
5
|
const algorithmMap = {
|
|
5
6
|
sha1: 'SHA-1',
|
|
6
7
|
sha256: 'SHA-256',
|
package/dist/node.d.ts
CHANGED
|
@@ -1,43 +1,18 @@
|
|
|
1
1
|
import type { SignatureAlgorithm } from './index.ts';
|
|
2
2
|
import type { SmartCdnUrlOptions } from './smartCdn.ts';
|
|
3
|
+
import type { SmartCdnImageCandidates, SmartCdnImagePolicyOptions } from './smartCdnImage.ts';
|
|
3
4
|
export type { SignatureAlgorithm } from './index.ts';
|
|
4
5
|
export type { ParsedSmartCdnUrl, ParseSmartCdnUrlOptions, SmartCdnUnsignedUrlOptions, SmartCdnUrlOptions, SmartCdnUrlParams, } from './smartCdn.ts';
|
|
6
|
+
export type { SignSmartCdnImageRequest, SmartCdnImageCandidate, SmartCdnImageCandidates, SmartCdnImageFormat, SmartCdnImageFormatQuality, SmartCdnImageFormats, SmartCdnImagePolicyOptions, SmartCdnImageSignRequest, SmartCdnImageSource, SmartCdnImageSourceDimensions, } from './smartCdnImage.ts';
|
|
5
7
|
export { getSmartCdnUrl, parseSmartCdnUrl, stripSmartCdnAuth } from './smartCdn.ts';
|
|
8
|
+
export { resolveSmartCdnImageFormats, resolveSmartCdnImageWidths, smartCdnImageMaxDimension, } from './smartCdnImage.ts';
|
|
6
9
|
export type SignatureAlgorithmInput = SignatureAlgorithm | (string & {});
|
|
7
|
-
/** Image formats supported by the responsive-image Built-in. */
|
|
8
|
-
export type SmartCdnImageFormat = 'avif' | 'png' | 'webp';
|
|
9
|
-
/** One signed Smart CDN rendition at a specific intrinsic width. */
|
|
10
|
-
export interface SmartCdnImageCandidate {
|
|
11
|
-
url: string;
|
|
12
|
-
width: number;
|
|
13
|
-
}
|
|
14
|
-
/** Ordered candidates for one image format and quality. */
|
|
15
|
-
export interface SmartCdnImageSource {
|
|
16
|
-
candidates: readonly SmartCdnImageCandidate[];
|
|
17
|
-
format: SmartCdnImageFormat;
|
|
18
|
-
quality: number;
|
|
19
|
-
}
|
|
20
|
-
/** Structured data for rendering a responsive image. */
|
|
21
|
-
export interface SmartCdnImageCandidates {
|
|
22
|
-
fallbackUrl: string;
|
|
23
|
-
sources: readonly SmartCdnImageSource[];
|
|
24
|
-
}
|
|
25
10
|
/** Options for deterministic, server-generated Smart CDN image candidates. */
|
|
26
|
-
export interface SmartCdnImageCandidatesOptions {
|
|
11
|
+
export interface SmartCdnImageCandidatesOptions extends SmartCdnImagePolicyOptions {
|
|
27
12
|
/** Transloadit auth key used to sign every candidate URL. */
|
|
28
13
|
authKey: string;
|
|
29
14
|
/** Transloadit auth secret used to sign every candidate URL. */
|
|
30
15
|
authSecret: string;
|
|
31
|
-
/** One absolute expiry in milliseconds since UNIX epoch, shared by every candidate. */
|
|
32
|
-
expiresAt: number;
|
|
33
|
-
/** Formats and their quality values. Defaults to AVIF 45 and WebP 75. */
|
|
34
|
-
formats?: Readonly<Partial<Record<SmartCdnImageFormat, number>>>;
|
|
35
|
-
/** Absolute HTTP(S) source URL accepted by the responsive-image Template. */
|
|
36
|
-
input: string;
|
|
37
|
-
/** Compatible Template override. Defaults to `builtin/serve-image@0.0.1`. */
|
|
38
|
-
template?: string;
|
|
39
|
-
/** Up to 32 intrinsic widths. Each value must be an integer from 1 through 8000. */
|
|
40
|
-
widths: readonly number[];
|
|
41
16
|
/** Workspace slug. */
|
|
42
17
|
workspace: string;
|
|
43
18
|
}
|
|
@@ -47,8 +22,8 @@ export declare const getSignedSmartCdnUrl: (opts: SmartCdnUrlOptions) => string;
|
|
|
47
22
|
/**
|
|
48
23
|
* Builds deterministic signed Smart CDN candidates for server-rendered `<picture>` elements.
|
|
49
24
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
25
|
+
* Pass `sourceDimensions` when known so width descriptors remain truthful without producing a
|
|
26
|
+
* rendition above the backend's width or derived-height limits.
|
|
52
27
|
*/
|
|
53
28
|
export declare function getSignedSmartCdnImageCandidates(opts: SmartCdnImageCandidatesOptions): SmartCdnImageCandidates;
|
|
54
29
|
//# sourceMappingURL=node.d.ts.map
|
package/dist/node.d.ts.map
CHANGED
|
@@ -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;AACpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AACpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AACvD,OAAO,KAAK,EAAE,uBAAuB,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAA;AAO7F,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AACpD,YAAY,EACV,iBAAiB,EACjB,uBAAuB,EACvB,0BAA0B,EAC1B,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,eAAe,CAAA;AACtB,YAAY,EACV,wBAAwB,EACxB,sBAAsB,EACtB,uBAAuB,EACvB,mBAAmB,EACnB,0BAA0B,EAC1B,oBAAoB,EACpB,0BAA0B,EAC1B,wBAAwB,EACxB,mBAAmB,EACnB,6BAA6B,GAC9B,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACnF,OAAO,EACL,2BAA2B,EAC3B,0BAA0B,EAC1B,yBAAyB,GAC1B,MAAM,oBAAoB,CAAA;AAE3B,MAAM,MAAM,uBAAuB,GAAG,kBAAkB,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAExE,8EAA8E;AAC9E,MAAM,WAAW,8BAA+B,SAAQ,0BAA0B;IAChF,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAA;IACf,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAA;IAClB,sBAAsB;IACtB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,eAAO,MAAM,cAAc,GACzB,cAAc,MAAM,EACpB,YAAY,MAAM,EAClB,YAAW,uBAAkC,KAC5C,MAKF,CAAA;AAED,4FAA4F;AAC5F,eAAO,MAAM,oBAAoB,GAAI,MAAM,kBAAkB,KAAG,MAM/D,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE,8BAA8B,GACnC,uBAAuB,CAsBzB"}
|
package/dist/node.js
CHANGED
|
@@ -1,58 +1,8 @@
|
|
|
1
1
|
import { createHmac } from 'node:crypto';
|
|
2
2
|
import { finishSmartCdnUrl, prepareSmartCdnUrl } from "./smartCdn.js";
|
|
3
|
+
import { createSmartCdnImageCandidates } from "./smartCdnImage.js";
|
|
3
4
|
export { getSmartCdnUrl, parseSmartCdnUrl, stripSmartCdnAuth } from "./smartCdn.js";
|
|
4
|
-
|
|
5
|
-
avif: 45,
|
|
6
|
-
webp: 75,
|
|
7
|
-
};
|
|
8
|
-
const defaultSmartCdnImageTemplate = 'builtin/serve-image@0.0.1';
|
|
9
|
-
const smartCdnImageFormats = ['avif', 'webp', 'png'];
|
|
10
|
-
const smartCdnImageMaxDimension = 8000;
|
|
11
|
-
const smartCdnImageMaxWidths = 32;
|
|
12
|
-
function isSmartCdnImageFormat(value) {
|
|
13
|
-
return value === 'avif' || value === 'png' || value === 'webp';
|
|
14
|
-
}
|
|
15
|
-
function validateSmartCdnImageDimension(width) {
|
|
16
|
-
if (!Number.isInteger(width) || width < 1 || width > smartCdnImageMaxDimension) {
|
|
17
|
-
throw new RangeError(`width must be an integer from 1 through ${smartCdnImageMaxDimension}`);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
function validateSmartCdnImageQuality(quality) {
|
|
21
|
-
if (!Number.isInteger(quality) || quality < 1 || quality > 100) {
|
|
22
|
-
throw new RangeError('quality must be an integer from 1 through 100');
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
function validateSmartCdnImageInput(input) {
|
|
26
|
-
if (typeof input !== 'string' || input.trim() !== input || input.includes('|')) {
|
|
27
|
-
throw new TypeError('input must be a single HTTP or HTTPS URL string');
|
|
28
|
-
}
|
|
29
|
-
if (!URL.canParse(input)) {
|
|
30
|
-
throw new TypeError('input must be an HTTP or HTTPS URL');
|
|
31
|
-
}
|
|
32
|
-
const protocol = new URL(input).protocol;
|
|
33
|
-
if (protocol !== 'http:' && protocol !== 'https:') {
|
|
34
|
-
throw new TypeError('input must be an HTTP or HTTPS URL');
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
function validateSmartCdnImageFormats(formats) {
|
|
38
|
-
for (const format of Object.keys(formats)) {
|
|
39
|
-
if (!isSmartCdnImageFormat(format)) {
|
|
40
|
-
throw new TypeError(`Unsupported Smart CDN image format: ${format}`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
let formatCount = 0;
|
|
44
|
-
for (const format of smartCdnImageFormats) {
|
|
45
|
-
const quality = formats[format];
|
|
46
|
-
if (quality == null) {
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
validateSmartCdnImageQuality(quality);
|
|
50
|
-
formatCount += 1;
|
|
51
|
-
}
|
|
52
|
-
if (formatCount === 0) {
|
|
53
|
-
throw new TypeError('formats must contain at least one value');
|
|
54
|
-
}
|
|
55
|
-
}
|
|
5
|
+
export { resolveSmartCdnImageFormats, resolveSmartCdnImageWidths, smartCdnImageMaxDimension, } from "./smartCdnImage.js";
|
|
56
6
|
export const signParamsSync = (paramsString, authSecret, algorithm = 'sha384') => {
|
|
57
7
|
const signature = createHmac(algorithm, authSecret)
|
|
58
8
|
.update(Buffer.from(paramsString, 'utf-8'))
|
|
@@ -70,56 +20,26 @@ export const getSignedSmartCdnUrl = (opts) => {
|
|
|
70
20
|
/**
|
|
71
21
|
* Builds deterministic signed Smart CDN candidates for server-rendered `<picture>` elements.
|
|
72
22
|
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
23
|
+
* Pass `sourceDimensions` when known so width descriptors remain truthful without producing a
|
|
24
|
+
* rendition above the backend's width or derived-height limits.
|
|
75
25
|
*/
|
|
76
26
|
export function getSignedSmartCdnImageCandidates(opts) {
|
|
77
|
-
|
|
27
|
+
const authKey = opts.authKey;
|
|
28
|
+
const authSecret = opts.authSecret;
|
|
29
|
+
const workspace = opts.workspace;
|
|
30
|
+
if (typeof authKey !== 'string' || authKey === '') {
|
|
78
31
|
throw new TypeError('authKey is required');
|
|
79
32
|
}
|
|
80
|
-
if (typeof
|
|
33
|
+
if (typeof authSecret !== 'string' || authSecret === '') {
|
|
81
34
|
throw new TypeError('authSecret is required');
|
|
82
35
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
validateSmartCdnImageInput(opts.input);
|
|
93
|
-
const widths = [...new Set(opts.widths)];
|
|
94
|
-
if (widths.length > smartCdnImageMaxWidths) {
|
|
95
|
-
throw new RangeError(`widths must contain at most ${smartCdnImageMaxWidths} unique values`);
|
|
96
|
-
}
|
|
97
|
-
for (const width of widths) {
|
|
98
|
-
validateSmartCdnImageDimension(width);
|
|
99
|
-
}
|
|
100
|
-
const formats = opts.formats ?? defaultSmartCdnImageFormats;
|
|
101
|
-
validateSmartCdnImageFormats(formats);
|
|
102
|
-
widths.sort((left, right) => left - right);
|
|
103
|
-
const sources = [];
|
|
104
|
-
for (const format of smartCdnImageFormats) {
|
|
105
|
-
const quality = formats[format];
|
|
106
|
-
if (quality == null) {
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
const candidates = [];
|
|
110
|
-
for (const width of widths) {
|
|
111
|
-
const url = getSignedSmartCdnUrl({
|
|
112
|
-
authKey: opts.authKey,
|
|
113
|
-
authSecret: opts.authSecret,
|
|
114
|
-
expiresAt: opts.expiresAt,
|
|
115
|
-
input: opts.input,
|
|
116
|
-
template: opts.template ?? defaultSmartCdnImageTemplate,
|
|
117
|
-
urlParams: { f: format, q: quality, r: 'fit', w: width },
|
|
118
|
-
workspace: opts.workspace,
|
|
119
|
-
});
|
|
120
|
-
candidates.push({ url, width });
|
|
121
|
-
}
|
|
122
|
-
sources.push({ candidates, format, quality });
|
|
123
|
-
}
|
|
124
|
-
return { fallbackUrl: opts.input, sources };
|
|
36
|
+
return createSmartCdnImageCandidates(opts, (request) => getSignedSmartCdnUrl({
|
|
37
|
+
authKey,
|
|
38
|
+
authSecret,
|
|
39
|
+
expiresAt: request.expiresAt,
|
|
40
|
+
input: request.input,
|
|
41
|
+
template: request.template,
|
|
42
|
+
urlParams: { ...request.urlParams },
|
|
43
|
+
workspace,
|
|
44
|
+
}));
|
|
125
45
|
}
|
package/dist/smartCdn.d.ts
CHANGED
|
@@ -19,7 +19,8 @@ export type SmartCdnUrlOptions = {
|
|
|
19
19
|
*/
|
|
20
20
|
input: string;
|
|
21
21
|
/**
|
|
22
|
-
* Additional parameters for the URL query string.
|
|
22
|
+
* Additional parameters for the URL query string. `auth_key`, `exp`, and `sig` are reserved:
|
|
23
|
+
* signed builders replace them and unsigned builders omit them.
|
|
23
24
|
*/
|
|
24
25
|
urlParams?: SmartCdnUrlParams;
|
|
25
26
|
/**
|
|
@@ -67,7 +68,7 @@ export interface ParsedSmartCdnUrl {
|
|
|
67
68
|
workspace: string;
|
|
68
69
|
template: string;
|
|
69
70
|
input: string;
|
|
70
|
-
/** Every query parameter except
|
|
71
|
+
/** Every query parameter except auth fields; repeated parameters become arrays. */
|
|
71
72
|
urlParams: Record<string, string | string[]>;
|
|
72
73
|
/** Present when the URL carries `auth_key`, `exp` and `sig`. */
|
|
73
74
|
auth?: {
|
package/dist/smartCdn.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"smartCdn.d.ts","sourceRoot":"","sources":["../src/smartCdn.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CACpC,MAAM,EACN,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,CAAC,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC,EAAE,CAC1D,CAAA;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
|
|
1
|
+
{"version":3,"file":"smartCdn.d.ts","sourceRoot":"","sources":["../src/smartCdn.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CACpC,MAAM,EACN,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,CAAC,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC,EAAE,CAC1D,CAAA;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;;;OAGG;IACH,SAAS,CAAC,EAAE,iBAAiB,CAAA;IAC7B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAA;IACf;;OAEG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,+FAA+F;AAC/F,MAAM,MAAM,0BAA0B,GAAG,IAAI,CAC3C,kBAAkB,EAClB,SAAS,GAAG,YAAY,GAAG,WAAW,CACvC,CAAA;AAED,kEAAkE;AAClE,MAAM,WAAW,mBAAmB;IAClC,kGAAkG;IAClG,YAAY,EAAE,MAAM,CAAA;IACpB,sEAAsE;IACtE,KAAK,EAAE;QACL,aAAa,EAAE,MAAM,CAAA;QACrB,YAAY,EAAE,MAAM,CAAA;QACpB,UAAU,EAAE,MAAM,CAAA;QAClB,WAAW,EAAE,eAAe,CAAA;QAC5B,yEAAyE;QACzE,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;CACF;AAED,4EAA4E;AAC5E,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,mFAAmF;IACnF,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAA;IAC5C,gEAAgE;IAChE,IAAI,CAAC,EAAE;QACL,GAAG,EAAE,MAAM,CAAA;QACX,qCAAqC;QACrC,SAAS,EAAE,MAAM,CAAA;QACjB,wCAAwC;QACxC,SAAS,EAAE,MAAM,CAAA;KAClB,CAAA;IACD,0GAA0G;IAC1G,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,qGAAqG;IACrG,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAyCD,4FAA4F;AAC5F,eAAO,MAAM,kBAAkB,GAAI,MAAM,kBAAkB,KAAG,mBAyB7D,CAAA;AAED,+FAA+F;AAC/F,eAAO,MAAM,iBAAiB,GAAI,WAAW,mBAAmB,EAAE,cAAc,MAAM,KAAG,MAIxF,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,0BAA0B,KAAG,MAajE,CAAA;AAUD;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,KAAG,MAsB/C,CAAA;AAwDD;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAC3B,KAAK,MAAM,EACX,UAAS,uBAA4B,KACpC,iBA4DF,CAAA"}
|
package/dist/smartCdn.js
CHANGED
|
@@ -55,6 +55,8 @@ export const prepareSmartCdnUrl = (opts) => {
|
|
|
55
55
|
const inputField = encodeURIComponent(opts.input);
|
|
56
56
|
const expiresAt = opts.expiresAt || Date.now() + 60 * 60 * 1000;
|
|
57
57
|
const queryParams = buildQueryParams(opts.urlParams);
|
|
58
|
+
// Keep accepting legacy values: the signer safely replaces its own authentication fields.
|
|
59
|
+
queryParams.delete('sig');
|
|
58
60
|
queryParams.set('auth_key', opts.authKey);
|
|
59
61
|
queryParams.set('exp', `${expiresAt}`);
|
|
60
62
|
queryParams.sort();
|
|
@@ -85,6 +87,9 @@ export const getSmartCdnUrl = (opts) => {
|
|
|
85
87
|
const templateSlug = encodeURIComponent(opts.template);
|
|
86
88
|
const inputField = encodeURIComponent(opts.input);
|
|
87
89
|
const queryParams = buildQueryParams(opts.urlParams);
|
|
90
|
+
// An unsigned builder must not emit fields that make the URL look partially or fully signed.
|
|
91
|
+
for (const param of SIGNATURE_PARAMS)
|
|
92
|
+
queryParams.delete(param);
|
|
88
93
|
queryParams.sort();
|
|
89
94
|
const query = queryParams.toString();
|
|
90
95
|
return `${resolveBaseUrl(opts.baseUrl, workspaceSlug)}/${templateSlug}/${inputField}${query === '' ? '' : `?${query}`}`;
|
|
@@ -190,10 +195,20 @@ export const parseSmartCdnUrl = (url, options = {}) => {
|
|
|
190
195
|
if (templateSlug === '')
|
|
191
196
|
throw notSmartCdnUrl('missing the template segment');
|
|
192
197
|
const urlParams = {};
|
|
193
|
-
|
|
198
|
+
let authKey;
|
|
199
|
+
let expiration;
|
|
200
|
+
let signatureValue;
|
|
194
201
|
for (const [key, value] of new URLSearchParams(parsed.search)) {
|
|
195
|
-
if (
|
|
196
|
-
|
|
202
|
+
if (key === 'auth_key') {
|
|
203
|
+
authKey = value;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (key === 'exp') {
|
|
207
|
+
expiration = value;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (key === 'sig') {
|
|
211
|
+
signatureValue = value;
|
|
197
212
|
continue;
|
|
198
213
|
}
|
|
199
214
|
const existing = urlParams[key];
|
|
@@ -205,15 +220,15 @@ export const parseSmartCdnUrl = (url, options = {}) => {
|
|
|
205
220
|
urlParams[key] = [existing, value];
|
|
206
221
|
}
|
|
207
222
|
let auth;
|
|
208
|
-
const present =
|
|
223
|
+
const present = [authKey, expiration, signatureValue].filter((value) => value !== undefined).length;
|
|
209
224
|
if (present > 0) {
|
|
210
|
-
if (
|
|
225
|
+
if (authKey === undefined || expiration === undefined || signatureValue === undefined) {
|
|
211
226
|
throw notSmartCdnUrl('incomplete signature parameters; expected auth_key, exp and sig together');
|
|
212
227
|
}
|
|
213
|
-
const expiresAt = Number(
|
|
228
|
+
const expiresAt = Number(expiration);
|
|
214
229
|
if (!Number.isInteger(expiresAt))
|
|
215
|
-
throw notSmartCdnUrl(`exp '${
|
|
216
|
-
auth = { key:
|
|
230
|
+
throw notSmartCdnUrl(`exp '${expiration}' is not a timestamp`);
|
|
231
|
+
auth = { key: authKey, expiresAt, signature: signatureValue };
|
|
217
232
|
}
|
|
218
233
|
return {
|
|
219
234
|
workspace: decodeOnce(workspaceSlug, 'the workspace'),
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Maximum requested width or height accepted by the responsive-image Built-ins. */
|
|
2
|
+
export declare const smartCdnImageMaxDimension = 8000;
|
|
3
|
+
/** Image formats supported by the responsive-image Built-in. */
|
|
4
|
+
export type SmartCdnImageFormat = 'avif' | 'png' | 'webp';
|
|
5
|
+
/** Formats and their format-specific quality values. */
|
|
6
|
+
export type SmartCdnImageFormats = Readonly<Partial<Record<SmartCdnImageFormat, number>>>;
|
|
7
|
+
/** One responsive-image candidate at a specific intrinsic width. */
|
|
8
|
+
export interface SmartCdnImageCandidate {
|
|
9
|
+
url: string;
|
|
10
|
+
width: number;
|
|
11
|
+
}
|
|
12
|
+
/** Ordered candidates for one image format and quality. */
|
|
13
|
+
export interface SmartCdnImageSource {
|
|
14
|
+
candidates: readonly SmartCdnImageCandidate[];
|
|
15
|
+
format: SmartCdnImageFormat;
|
|
16
|
+
quality: number;
|
|
17
|
+
}
|
|
18
|
+
/** One validated format and its encoding quality, in browser preference order. */
|
|
19
|
+
export interface SmartCdnImageFormatQuality {
|
|
20
|
+
format: SmartCdnImageFormat;
|
|
21
|
+
quality: number;
|
|
22
|
+
}
|
|
23
|
+
/** Structured data for rendering a responsive image. */
|
|
24
|
+
export interface SmartCdnImageCandidates {
|
|
25
|
+
fallbackUrl: string;
|
|
26
|
+
sources: readonly SmartCdnImageSource[];
|
|
27
|
+
}
|
|
28
|
+
/** Intrinsic dimensions used to prevent upscaling or an oversized derived height. */
|
|
29
|
+
export interface SmartCdnImageSourceDimensions {
|
|
30
|
+
height: number;
|
|
31
|
+
width: number;
|
|
32
|
+
}
|
|
33
|
+
/** One rendition request passed to an injected Smart CDN signer. */
|
|
34
|
+
export interface SmartCdnImageSignRequest {
|
|
35
|
+
expiresAt: number;
|
|
36
|
+
input: string;
|
|
37
|
+
template: string;
|
|
38
|
+
urlParams: Readonly<Record<string, boolean | number | string>>;
|
|
39
|
+
}
|
|
40
|
+
/** Injected signer that keeps responsive-image policy independent from credentials and runtimes. */
|
|
41
|
+
export type SignSmartCdnImageRequest = (request: SmartCdnImageSignRequest) => string;
|
|
42
|
+
/** Framework-neutral options for deterministic Smart CDN image candidates. */
|
|
43
|
+
export interface SmartCdnImagePolicyOptions {
|
|
44
|
+
/** One absolute expiry in milliseconds since UNIX epoch, shared by every candidate. */
|
|
45
|
+
expiresAt: number;
|
|
46
|
+
/** Browser-safe fallback URL, kept separate from the Template-specific input value. */
|
|
47
|
+
fallbackUrl: string;
|
|
48
|
+
/** Formats and their quality values. Defaults to AVIF 45 and WebP 75. */
|
|
49
|
+
formats?: SmartCdnImageFormats;
|
|
50
|
+
/** One source value accepted by the explicitly selected responsive-image Template. */
|
|
51
|
+
input: string;
|
|
52
|
+
/** Intrinsic dimensions, when known, used to keep generated output within backend limits. */
|
|
53
|
+
sourceDimensions?: SmartCdnImageSourceDimensions;
|
|
54
|
+
/** Trusted Template whose source policy is controlled by the caller's workspace. */
|
|
55
|
+
template: string;
|
|
56
|
+
/** Up to 32 intrinsic widths. Each value must be an integer from 1 through 8000. */
|
|
57
|
+
widths: readonly number[];
|
|
58
|
+
}
|
|
59
|
+
/** Resolves and validates format-specific qualities in deterministic browser preference order. */
|
|
60
|
+
export declare function resolveSmartCdnImageFormats(formats: SmartCdnImageFormats | undefined): SmartCdnImageFormatQuality[];
|
|
61
|
+
/** Validates, caps, deduplicates, and sorts requested responsive-image widths. */
|
|
62
|
+
export declare function resolveSmartCdnImageWidths(widths: readonly number[], maximumWidth?: number): number[];
|
|
63
|
+
/**
|
|
64
|
+
* Creates signed responsive-image candidates while leaving credential storage and HMAC choice to
|
|
65
|
+
* the injected signer.
|
|
66
|
+
*/
|
|
67
|
+
export declare function createSmartCdnImageCandidates(options: SmartCdnImagePolicyOptions, sign: SignSmartCdnImageRequest): SmartCdnImageCandidates;
|
|
68
|
+
//# sourceMappingURL=smartCdnImage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smartCdnImage.d.ts","sourceRoot":"","sources":["../src/smartCdnImage.ts"],"names":[],"mappings":"AAKA,oFAAoF;AACpF,eAAO,MAAM,yBAAyB,OAAO,CAAA;AAE7C,gEAAgE;AAChE,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,CAAA;AAEzD,wDAAwD;AACxD,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;AAEzF,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,kFAAkF;AAClF,MAAM,WAAW,0BAA0B;IACzC,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,qFAAqF;AACrF,MAAM,WAAW,6BAA6B;IAC5C,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;CACd;AAED,oEAAoE;AACpE,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC,CAAC,CAAA;CAC/D;AAED,oGAAoG;AACpG,MAAM,MAAM,wBAAwB,GAAG,CAAC,OAAO,EAAE,wBAAwB,KAAK,MAAM,CAAA;AAEpF,8EAA8E;AAC9E,MAAM,WAAW,0BAA0B;IACzC,uFAAuF;IACvF,SAAS,EAAE,MAAM,CAAA;IACjB,uFAAuF;IACvF,WAAW,EAAE,MAAM,CAAA;IACnB,yEAAyE;IACzE,OAAO,CAAC,EAAE,oBAAoB,CAAA;IAC9B,sFAAsF;IACtF,KAAK,EAAE,MAAM,CAAA;IACb,6FAA6F;IAC7F,gBAAgB,CAAC,EAAE,6BAA6B,CAAA;IAChD,oFAAoF;IACpF,QAAQ,EAAE,MAAM,CAAA;IAChB,oFAAoF;IACpF,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAC1B;AA0CD,kGAAkG;AAClG,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,oBAAoB,GAAG,SAAS,GACxC,0BAA0B,EAAE,CAkB9B;AAqBD,kFAAkF;AAClF,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,YAAY,SAA4B,GACvC,MAAM,EAAE,CAeV;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,0BAA0B,EACnC,IAAI,EAAE,wBAAwB,GAC7B,uBAAuB,CAgDzB"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const defaultSmartCdnImageFormats = { avif: 45, webp: 75 };
|
|
2
|
+
const minimumMillisecondTimestamp = 1_000_000_000_000;
|
|
3
|
+
const smartCdnImageFormats = ['avif', 'webp', 'png'];
|
|
4
|
+
const smartCdnImageMaxWidths = 32;
|
|
5
|
+
/** Maximum requested width or height accepted by the responsive-image Built-ins. */
|
|
6
|
+
export const smartCdnImageMaxDimension = 8000;
|
|
7
|
+
function isSmartCdnImageFormat(value) {
|
|
8
|
+
return value === 'avif' || value === 'png' || value === 'webp';
|
|
9
|
+
}
|
|
10
|
+
function validatePositiveSafeInteger(value, name) {
|
|
11
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
12
|
+
throw new RangeError(`${name} must be a positive safe integer`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function validateSmartCdnImageDimension(value, name) {
|
|
16
|
+
if (!Number.isInteger(value) || value < 1 || value > smartCdnImageMaxDimension) {
|
|
17
|
+
throw new RangeError(`${name} must be an integer from 1 through ${smartCdnImageMaxDimension}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function validateSmartCdnImageQuality(quality) {
|
|
21
|
+
if (!Number.isInteger(quality) || quality < 1 || quality > 100) {
|
|
22
|
+
throw new RangeError('quality must be an integer from 1 through 100');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function validateSmartCdnImageInput(input) {
|
|
26
|
+
if (typeof input !== 'string' || input === '' || input.trim() !== input || input.includes('|')) {
|
|
27
|
+
throw new TypeError('input must be one non-empty Template input string');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function validateSmartCdnImageFallbackUrl(fallbackUrl) {
|
|
31
|
+
if (typeof fallbackUrl !== 'string' || fallbackUrl === '' || fallbackUrl.trim() !== fallbackUrl) {
|
|
32
|
+
throw new TypeError('fallbackUrl must be a non-empty string without surrounding whitespace');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function validateSmartCdnImageTemplate(template) {
|
|
36
|
+
if (typeof template !== 'string' || template === '' || template.trim() !== template) {
|
|
37
|
+
throw new TypeError('template must be a non-empty string without surrounding whitespace');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Resolves and validates format-specific qualities in deterministic browser preference order. */
|
|
41
|
+
export function resolveSmartCdnImageFormats(formats) {
|
|
42
|
+
const resolved = formats ?? defaultSmartCdnImageFormats;
|
|
43
|
+
for (const format of Object.keys(resolved)) {
|
|
44
|
+
if (!isSmartCdnImageFormat(format)) {
|
|
45
|
+
throw new TypeError(`Unsupported Smart CDN image format: ${format}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const selected = [];
|
|
49
|
+
for (const format of smartCdnImageFormats) {
|
|
50
|
+
if (!Object.hasOwn(resolved, format))
|
|
51
|
+
continue;
|
|
52
|
+
const quality = resolved[format];
|
|
53
|
+
if (quality === undefined)
|
|
54
|
+
continue;
|
|
55
|
+
validateSmartCdnImageQuality(quality);
|
|
56
|
+
selected.push({ format, quality });
|
|
57
|
+
}
|
|
58
|
+
if (selected.length === 0)
|
|
59
|
+
throw new TypeError('formats must contain at least one value');
|
|
60
|
+
return selected;
|
|
61
|
+
}
|
|
62
|
+
function getMaximumCandidateWidth(sourceDimensions) {
|
|
63
|
+
if (sourceDimensions === undefined)
|
|
64
|
+
return smartCdnImageMaxDimension;
|
|
65
|
+
validatePositiveSafeInteger(sourceDimensions.width, 'sourceDimensions.width');
|
|
66
|
+
validatePositiveSafeInteger(sourceDimensions.height, 'sourceDimensions.height');
|
|
67
|
+
const heightLimitedWidth = Number((BigInt(smartCdnImageMaxDimension) * BigInt(sourceDimensions.width)) /
|
|
68
|
+
BigInt(sourceDimensions.height));
|
|
69
|
+
if (heightLimitedWidth < 1) {
|
|
70
|
+
// Even a one-pixel-wide rendition would exceed the backend height limit; no truthful candidate
|
|
71
|
+
// can preserve this aspect ratio.
|
|
72
|
+
throw new RangeError('sourceDimensions aspect ratio cannot fit within backend dimensions');
|
|
73
|
+
}
|
|
74
|
+
return Math.min(smartCdnImageMaxDimension, sourceDimensions.width, heightLimitedWidth);
|
|
75
|
+
}
|
|
76
|
+
/** Validates, caps, deduplicates, and sorts requested responsive-image widths. */
|
|
77
|
+
export function resolveSmartCdnImageWidths(widths, maximumWidth = smartCdnImageMaxDimension) {
|
|
78
|
+
if (!Array.isArray(widths) || widths.length === 0) {
|
|
79
|
+
throw new TypeError('widths must contain at least one value');
|
|
80
|
+
}
|
|
81
|
+
if (widths.length > smartCdnImageMaxWidths) {
|
|
82
|
+
throw new RangeError(`widths must contain at most ${smartCdnImageMaxWidths} values`);
|
|
83
|
+
}
|
|
84
|
+
validateSmartCdnImageDimension(maximumWidth, 'maximumWidth');
|
|
85
|
+
const candidates = new Set();
|
|
86
|
+
for (const [index, width] of widths.entries()) {
|
|
87
|
+
validateSmartCdnImageDimension(width, `widths[${index}]`);
|
|
88
|
+
candidates.add(Math.min(width, maximumWidth));
|
|
89
|
+
}
|
|
90
|
+
return [...candidates].sort((left, right) => left - right);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Creates signed responsive-image candidates while leaving credential storage and HMAC choice to
|
|
94
|
+
* the injected signer.
|
|
95
|
+
*/
|
|
96
|
+
export function createSmartCdnImageCandidates(options, sign) {
|
|
97
|
+
const expiresAt = options.expiresAt;
|
|
98
|
+
const fallbackUrl = options.fallbackUrl;
|
|
99
|
+
const formatOptions = options.formats;
|
|
100
|
+
const formatsSnapshot = formatOptions === undefined ? undefined : { ...formatOptions };
|
|
101
|
+
const input = options.input;
|
|
102
|
+
const sourceDimensionOptions = options.sourceDimensions;
|
|
103
|
+
const sourceDimensions = sourceDimensionOptions === undefined
|
|
104
|
+
? undefined
|
|
105
|
+
: { height: sourceDimensionOptions.height, width: sourceDimensionOptions.width };
|
|
106
|
+
const template = options.template;
|
|
107
|
+
const widthOptions = options.widths;
|
|
108
|
+
const widthsSnapshot = Array.isArray(widthOptions) ? [...widthOptions] : widthOptions;
|
|
109
|
+
validatePositiveSafeInteger(expiresAt, 'expiresAt');
|
|
110
|
+
if (expiresAt < minimumMillisecondTimestamp) {
|
|
111
|
+
throw new RangeError('expiresAt must be a millisecond timestamp');
|
|
112
|
+
}
|
|
113
|
+
validateSmartCdnImageFallbackUrl(fallbackUrl);
|
|
114
|
+
validateSmartCdnImageInput(input);
|
|
115
|
+
validateSmartCdnImageTemplate(template);
|
|
116
|
+
if (typeof sign !== 'function')
|
|
117
|
+
throw new TypeError('sign must be a function');
|
|
118
|
+
const formats = resolveSmartCdnImageFormats(formatsSnapshot);
|
|
119
|
+
const widths = resolveSmartCdnImageWidths(widthsSnapshot, getMaximumCandidateWidth(sourceDimensions));
|
|
120
|
+
const sources = [];
|
|
121
|
+
for (const { format, quality } of formats) {
|
|
122
|
+
sources.push({
|
|
123
|
+
candidates: widths.map((width) => ({
|
|
124
|
+
url: sign({
|
|
125
|
+
expiresAt,
|
|
126
|
+
input,
|
|
127
|
+
template,
|
|
128
|
+
urlParams: { f: format, q: quality, r: 'fit', w: width },
|
|
129
|
+
}),
|
|
130
|
+
width,
|
|
131
|
+
})),
|
|
132
|
+
format,
|
|
133
|
+
quality,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return { fallbackUrl, sources };
|
|
137
|
+
}
|