@transloadit/utils 4.4.1 → 4.5.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/README.md +16 -1
- package/dist/node.d.ts +31 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +108 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -24,7 +24,11 @@ const verified = await verifyWebhookSignature({
|
|
|
24
24
|
## Node usage
|
|
25
25
|
|
|
26
26
|
```ts
|
|
27
|
-
import {
|
|
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,15 @@ 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
|
+
})
|
|
37
50
|
```
|
|
38
51
|
|
|
39
52
|
## API
|
|
@@ -42,3 +55,5 @@ const url = getSignedSmartCdnUrl({
|
|
|
42
55
|
- `verifyWebhookSignature({ rawBody, signatureHeader, authSecret })`: validates webhook signatures.
|
|
43
56
|
- `signParamsSync(paramsString, authSecret, algorithm?)`: Node-only sync signature helper.
|
|
44
57
|
- `getSignedSmartCdnUrl(options)`: Node-only Smart CDN URL signer.
|
|
58
|
+
- `getSignedSmartCdnImageCandidates(options)`: deterministic signed AVIF and WebP `srcset`
|
|
59
|
+
candidates plus the original fallback URL.
|
package/dist/node.d.ts
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import type { SignatureAlgorithm } from './index.ts';
|
|
2
2
|
export type { SignatureAlgorithm } from './index.ts';
|
|
3
3
|
export type SignatureAlgorithmInput = SignatureAlgorithm | (string & {});
|
|
4
|
+
type SmartCdnImageFormat = 'avif' | 'png' | 'webp';
|
|
5
|
+
interface SmartCdnImageCandidates {
|
|
6
|
+
fallback: string;
|
|
7
|
+
sources: Partial<Record<SmartCdnImageFormat, string>>;
|
|
8
|
+
}
|
|
9
|
+
/** Options for deterministic, server-generated Smart CDN image candidates. */
|
|
10
|
+
export interface SmartCdnImageCandidatesOptions {
|
|
11
|
+
/** Transloadit auth key used to sign every candidate URL. */
|
|
12
|
+
authKey: string;
|
|
13
|
+
/** Transloadit auth secret used to sign every candidate URL. */
|
|
14
|
+
authSecret: string;
|
|
15
|
+
/** One absolute expiry in milliseconds since UNIX epoch, shared by every candidate. */
|
|
16
|
+
expiresAt: number;
|
|
17
|
+
/** Formats and their quality values. Defaults to AVIF 45 and WebP 75. */
|
|
18
|
+
formats?: Readonly<Partial<Record<SmartCdnImageFormat, number>>>;
|
|
19
|
+
/** Absolute HTTP(S) source URL accepted by the responsive-image Template. */
|
|
20
|
+
input: string;
|
|
21
|
+
/** Compatible Template override. Defaults to `builtin/serve-image@0.0.1`. */
|
|
22
|
+
template?: string;
|
|
23
|
+
/** Up to 32 intrinsic widths. Each value must be an integer from 1 through 8000. */
|
|
24
|
+
widths: readonly number[];
|
|
25
|
+
/** Workspace slug. */
|
|
26
|
+
workspace: string;
|
|
27
|
+
}
|
|
4
28
|
export type SmartCdnUrlOptions = {
|
|
5
29
|
/**
|
|
6
30
|
* Workspace slug.
|
|
@@ -34,4 +58,11 @@ export type SmartCdnUrlOptions = {
|
|
|
34
58
|
};
|
|
35
59
|
export declare const signParamsSync: (paramsString: string, authSecret: string, algorithm?: SignatureAlgorithmInput) => string;
|
|
36
60
|
export declare const getSignedSmartCdnUrl: (opts: SmartCdnUrlOptions) => string;
|
|
61
|
+
/**
|
|
62
|
+
* Builds deterministic signed Smart CDN candidates for server-rendered `<picture>` elements.
|
|
63
|
+
*
|
|
64
|
+
* Width descriptors are only accurate when callers do not request widths above the source image's
|
|
65
|
+
* intrinsic width. The helper deliberately keeps the Built-in in width-only `fit` mode.
|
|
66
|
+
*/
|
|
67
|
+
export declare function getSignedSmartCdnImageCandidates(opts: SmartCdnImageCandidatesOptions): SmartCdnImageCandidates;
|
|
37
68
|
//# 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;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;
|
|
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,KAAK,mBAAmB,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,CAAA;AAElD,UAAU,uBAAuB;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC,CAAA;CACtD;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}w`);
|
|
141
|
+
}
|
|
142
|
+
sources[format] = candidates.join(', ');
|
|
143
|
+
}
|
|
144
|
+
return { fallback: opts.input, sources };
|
|
145
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@transloadit/utils",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
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": {
|