@transloadit/utils 4.7.0 → 4.8.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 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: 'https://example.com/image.jpg',
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 the original fallback URL.
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;
@@ -15,4 +17,6 @@ export declare const verifyWebhookSignature: (options: VerifyWebhookSignatureOpt
15
17
  * Produces the same URL as the synchronous `getSignedSmartCdnUrl` from `@transloadit/utils/node`.
16
18
  */
17
19
  export declare const getSignedSmartCdnUrl: (opts: SmartCdnUrlOptions) => Promise<string>;
20
+ export type { StorageGrantClaims, StorageGrantScope } from './storageGrant.ts';
21
+ export { decodeStorageGrant, normalizeStorageGrantPrefix, parseStorageGrantClaims, STORAGE_GRANT_SCOPES, } from './storageGrant.ts';
18
22
  //# sourceMappingURL=index.d.ts.map
@@ -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;AAEtB,cAAc,mCAAmC,CAAA;AACjD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAqDnF,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"}
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;AAED,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAE9E,OAAO,EACL,kBAAkB,EAClB,2BAA2B,EAC3B,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,mBAAmB,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',
@@ -68,3 +69,4 @@ export const getSignedSmartCdnUrl = async (opts) => {
68
69
  const signature = await hmacHex('sha256', opts.authSecret, prepared.stringToSign);
69
70
  return finishSmartCdnUrl(prepared, signature);
70
71
  };
72
+ export { decodeStorageGrant, normalizeStorageGrantPrefix, parseStorageGrantClaims, STORAGE_GRANT_SCOPES, } from "./storageGrant.js";
package/dist/node.d.ts CHANGED
@@ -1,43 +1,19 @@
1
1
  import type { SignatureAlgorithm } from './index.ts';
2
2
  import type { SmartCdnUrlOptions } from './smartCdn.ts';
3
+ import type { SmartCdnImageCandidates, SmartCdnImagePolicyOptions } from './smartCdnImage.ts';
4
+ import type { StorageGrantClaims, StorageGrantScope } from './storageGrant.ts';
3
5
  export type { SignatureAlgorithm } from './index.ts';
4
6
  export type { ParsedSmartCdnUrl, ParseSmartCdnUrlOptions, SmartCdnUnsignedUrlOptions, SmartCdnUrlOptions, SmartCdnUrlParams, } from './smartCdn.ts';
7
+ export type { SignSmartCdnImageRequest, SmartCdnImageCandidate, SmartCdnImageCandidates, SmartCdnImageFormat, SmartCdnImageFormatQuality, SmartCdnImageFormats, SmartCdnImagePolicyOptions, SmartCdnImageSignRequest, SmartCdnImageSource, SmartCdnImageSourceDimensions, } from './smartCdnImage.ts';
5
8
  export { getSmartCdnUrl, parseSmartCdnUrl, stripSmartCdnAuth } from './smartCdn.ts';
9
+ export { resolveSmartCdnImageFormats, resolveSmartCdnImageWidths, smartCdnImageMaxDimension, } from './smartCdnImage.ts';
6
10
  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
11
  /** Options for deterministic, server-generated Smart CDN image candidates. */
26
- export interface SmartCdnImageCandidatesOptions {
12
+ export interface SmartCdnImageCandidatesOptions extends SmartCdnImagePolicyOptions {
27
13
  /** Transloadit auth key used to sign every candidate URL. */
28
14
  authKey: string;
29
15
  /** Transloadit auth secret used to sign every candidate URL. */
30
16
  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
17
  /** Workspace slug. */
42
18
  workspace: string;
43
19
  }
@@ -47,8 +23,39 @@ export declare const getSignedSmartCdnUrl: (opts: SmartCdnUrlOptions) => string;
47
23
  /**
48
24
  * Builds deterministic signed Smart CDN candidates for server-rendered `<picture>` elements.
49
25
  *
50
- * Width descriptors are only accurate when callers do not request widths above the source image's
51
- * intrinsic width. The helper deliberately keeps the Built-in in width-only `fit` mode.
26
+ * Pass `sourceDimensions` when known so width descriptors remain truthful without producing a
27
+ * rendition above the backend's width or derived-height limits.
52
28
  */
53
29
  export declare function getSignedSmartCdnImageCandidates(opts: SmartCdnImageCandidatesOptions): SmartCdnImageCandidates;
30
+ export type { StorageGrantClaims, StorageGrantScope } from './storageGrant.ts';
31
+ export { decodeStorageGrant, normalizeStorageGrantPrefix, parseStorageGrantClaims, STORAGE_GRANT_SCOPES, } from './storageGrant.ts';
32
+ export type SignStorageGrantOptions = {
33
+ /** The workspace slug ("bucket" in S3 terms). */
34
+ bucket: string;
35
+ /** Key prefix to confine the session to. Default: the whole workspace. */
36
+ prefix?: string;
37
+ /** Default: read-only. */
38
+ scopes?: StorageGrantScope[];
39
+ /** Who the grant is minted for (informational). */
40
+ sub?: string;
41
+ /** Grant lifetime. Default: 900 (15 minutes). */
42
+ expiresInSeconds?: number;
43
+ /** Clock override for tests. */
44
+ nowMs?: number;
45
+ };
46
+ /**
47
+ * Mints a storage grant: an HS256 JWT with the v1 claim set, byte-compatible
48
+ * with api2's `StorageGrantManager` (same header, claim order and encoding).
49
+ */
50
+ export declare const signStorageGrant: (options: SignStorageGrantOptions, secret: string) => {
51
+ grant: string;
52
+ claims: StorageGrantClaims;
53
+ };
54
+ /**
55
+ * Verifies a storage grant: HS256 signature (timing-safe), strict v1 claim
56
+ * shape, and expiry. Throws with a stable message on any failure.
57
+ */
58
+ export declare const verifyStorageGrant: (token: string, secret: string, { nowMs }?: {
59
+ nowMs?: number;
60
+ }) => StorageGrantClaims;
54
61
  //# 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;AACpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAMvD,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AACpD,YAAY,EACV,iBAAiB,EACjB,uBAAuB,EACvB,0BAA0B,EAC1B,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,eAAe,CAAA;AAEtB,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAEnF,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;AAkED,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,CAsDzB"}
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;AAC7F,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAS9E,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;AAID,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAE9E,OAAO,EACL,kBAAkB,EAClB,2BAA2B,EAC3B,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,mBAAmB,CAAA;AAK1B,MAAM,MAAM,uBAAuB,GAAG;IACpC,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAA;IACd,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0BAA0B;IAC1B,MAAM,CAAC,EAAE,iBAAiB,EAAE,CAAA;IAC5B,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,iDAAiD;IACjD,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,gCAAgC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAC3B,SAAS,uBAAuB,EAChC,QAAQ,MAAM,KACb;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,kBAAkB,CAAA;CAiB7C,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAC7B,OAAO,MAAM,EACb,QAAQ,MAAM,EACd,YAAwB;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,KAC9C,kBA6BF,CAAA"}
package/dist/node.js CHANGED
@@ -1,58 +1,10 @@
1
- import { createHmac } from 'node:crypto';
1
+ import { Buffer } from 'node:buffer';
2
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
3
  import { finishSmartCdnUrl, prepareSmartCdnUrl } from "./smartCdn.js";
4
+ import { createSmartCdnImageCandidates } from "./smartCdnImage.js";
5
+ import { parseStorageGrantClaims } from "./storageGrant.js";
3
6
  export { getSmartCdnUrl, parseSmartCdnUrl, stripSmartCdnAuth } from "./smartCdn.js";
4
- const defaultSmartCdnImageFormats = {
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
- }
7
+ export { resolveSmartCdnImageFormats, resolveSmartCdnImageWidths, smartCdnImageMaxDimension, } from "./smartCdnImage.js";
56
8
  export const signParamsSync = (paramsString, authSecret, algorithm = 'sha384') => {
57
9
  const signature = createHmac(algorithm, authSecret)
58
10
  .update(Buffer.from(paramsString, 'utf-8'))
@@ -70,56 +22,83 @@ export const getSignedSmartCdnUrl = (opts) => {
70
22
  /**
71
23
  * Builds deterministic signed Smart CDN candidates for server-rendered `<picture>` elements.
72
24
  *
73
- * Width descriptors are only accurate when callers do not request widths above the source image's
74
- * intrinsic width. The helper deliberately keeps the Built-in in width-only `fit` mode.
25
+ * Pass `sourceDimensions` when known so width descriptors remain truthful without producing a
26
+ * rendition above the backend's width or derived-height limits.
75
27
  */
76
28
  export function getSignedSmartCdnImageCandidates(opts) {
77
- if (typeof opts.authKey !== 'string' || opts.authKey === '') {
29
+ const authKey = opts.authKey;
30
+ const authSecret = opts.authSecret;
31
+ const workspace = opts.workspace;
32
+ if (typeof authKey !== 'string' || authKey === '') {
78
33
  throw new TypeError('authKey is required');
79
34
  }
80
- if (typeof opts.authSecret !== 'string' || opts.authSecret === '') {
35
+ if (typeof authSecret !== 'string' || authSecret === '') {
81
36
  throw new TypeError('authSecret is required');
82
37
  }
83
- if (!Number.isSafeInteger(opts.expiresAt) || opts.expiresAt <= 0) {
84
- throw new RangeError('expiresAt must be a positive safe integer');
85
- }
86
- if (!Array.isArray(opts.widths) || opts.widths.length === 0) {
87
- throw new TypeError('widths must contain at least one value');
88
- }
89
- if (opts.widths.length > smartCdnImageMaxWidths) {
90
- throw new RangeError(`widths must contain at most ${smartCdnImageMaxWidths} values`);
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 };
38
+ return createSmartCdnImageCandidates(opts, (request) => getSignedSmartCdnUrl({
39
+ authKey,
40
+ authSecret,
41
+ expiresAt: request.expiresAt,
42
+ input: request.input,
43
+ template: request.template,
44
+ urlParams: { ...request.urlParams },
45
+ workspace,
46
+ }));
125
47
  }
48
+ export { decodeStorageGrant, normalizeStorageGrantPrefix, parseStorageGrantClaims, STORAGE_GRANT_SCOPES, } from "./storageGrant.js";
49
+ const base64url = (value) => (typeof value === 'string' ? Buffer.from(value) : value).toString('base64url');
50
+ /**
51
+ * Mints a storage grant: an HS256 JWT with the v1 claim set, byte-compatible
52
+ * with api2's `StorageGrantManager` (same header, claim order and encoding).
53
+ */
54
+ export const signStorageGrant = (options, secret) => {
55
+ const { bucket, prefix = '', scopes = ['read'], sub, expiresInSeconds = 900 } = options;
56
+ const iat = Math.floor((options.nowMs ?? Date.now()) / 1000);
57
+ const claims = {
58
+ v: 1,
59
+ bucket,
60
+ prefix,
61
+ scopes: [...new Set(scopes)],
62
+ ...(sub === undefined ? {} : { sub }),
63
+ iat,
64
+ exp: iat + expiresInSeconds,
65
+ };
66
+ const signingInput = `${base64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))}.${base64url(JSON.stringify(claims))}`;
67
+ const signature = base64url(createHmac('sha256', secret).update(signingInput).digest());
68
+ return { grant: `${signingInput}.${signature}`, claims };
69
+ };
70
+ /**
71
+ * Verifies a storage grant: HS256 signature (timing-safe), strict v1 claim
72
+ * shape, and expiry. Throws with a stable message on any failure.
73
+ */
74
+ export const verifyStorageGrant = (token, secret, { nowMs = Date.now() } = {}) => {
75
+ const [headerPart, payloadPart, signaturePart, ...rest] = token.split('.');
76
+ if (!headerPart || !payloadPart || !signaturePart || rest.length > 0) {
77
+ throw new Error('Invalid storage grant');
78
+ }
79
+ const expected = createHmac('sha256', secret).update(`${headerPart}.${payloadPart}`).digest();
80
+ const actual = Buffer.from(signaturePart, 'base64url');
81
+ if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
82
+ throw new Error('Invalid storage grant signature');
83
+ }
84
+ let header;
85
+ let payload;
86
+ try {
87
+ header = JSON.parse(Buffer.from(headerPart, 'base64url').toString('utf8'));
88
+ payload = JSON.parse(Buffer.from(payloadPart, 'base64url').toString('utf8'));
89
+ }
90
+ catch {
91
+ throw new Error('Invalid storage grant');
92
+ }
93
+ if (typeof header !== 'object' ||
94
+ header === null ||
95
+ header.alg !== 'HS256') {
96
+ throw new Error('Invalid storage grant algorithm');
97
+ }
98
+ const claims = parseStorageGrantClaims(payload);
99
+ if (claims === null)
100
+ throw new Error('Invalid storage grant claims');
101
+ if (claims.exp <= Math.floor(nowMs / 1000))
102
+ throw new Error('The storage grant has expired');
103
+ return claims;
104
+ };
@@ -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 the signature ones; repeated parameters become arrays. */
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?: {
@@ -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;;OAEG;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,0FAA0F;IAC1F,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,mBAuB7D,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,MAWjE,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,iBAiDF,CAAA"}
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
- const signature = {};
198
+ let authKey;
199
+ let expiration;
200
+ let signatureValue;
194
201
  for (const [key, value] of new URLSearchParams(parsed.search)) {
195
- if (SIGNATURE_PARAMS.has(key)) {
196
- signature[key] = value;
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 = Object.keys(signature).length;
223
+ const present = [authKey, expiration, signatureValue].filter((value) => value !== undefined).length;
209
224
  if (present > 0) {
210
- if (present !== SIGNATURE_PARAMS.size) {
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(signature.exp);
228
+ const expiresAt = Number(expiration);
214
229
  if (!Number.isInteger(expiresAt))
215
- throw notSmartCdnUrl(`exp '${signature.exp}' is not a timestamp`);
216
- auth = { key: signature.auth_key, expiresAt, signature: signature.sig };
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
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The Transloadit Storage grant: a short-lived HS256 JWT an integrator's
3
+ * server mints (or api2's `POST /storage/grants` mints for it) and
4
+ * Companion's S3 provider verifies. This module is the one wire contract —
5
+ * claim shape, decoding, and (in `@transloadit/utils/node`) minting and
6
+ * verification — so the implementations cannot drift apart.
7
+ *
8
+ * Everything in this file is browser-safe: the client may *read* a grant's
9
+ * claims to decide what UI to show, but verification is the server's job.
10
+ */
11
+ export declare const STORAGE_GRANT_SCOPES: readonly ["read", "write"];
12
+ export type StorageGrantScope = (typeof STORAGE_GRANT_SCOPES)[number];
13
+ export type StorageGrantClaims = {
14
+ v: 1;
15
+ /** The workspace slug ("bucket" in S3 terms). */
16
+ bucket: string;
17
+ /** Key prefix the session is confined to; may be empty. */
18
+ prefix: string;
19
+ scopes: StorageGrantScope[];
20
+ /** Who the grant was minted for (informational). */
21
+ sub?: string;
22
+ /** Unix seconds. */
23
+ iat?: number;
24
+ /** Unix seconds. */
25
+ exp: number;
26
+ };
27
+ /**
28
+ * Validates a decoded JWT payload against the grant contract. Returns the
29
+ * claims (scopes deduplicated) or null when the shape is not a v1 grant.
30
+ */
31
+ export declare const parseStorageGrantClaims: (payload: unknown) => StorageGrantClaims | null;
32
+ /** Companion's prefix policy: no leading slashes, a trailing slash unless empty. */
33
+ export declare const normalizeStorageGrantPrefix: (prefix: string) => string;
34
+ /**
35
+ * Reads a grant's payload without verifying the signature — for clients that
36
+ * only need to know what the session will be allowed to do. Returns null for
37
+ * anything that does not decode to a v1 grant.
38
+ */
39
+ export declare const decodeStorageGrant: (token: string) => StorageGrantClaims | null;
40
+ //# sourceMappingURL=storageGrant.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storageGrant.d.ts","sourceRoot":"","sources":["../src/storageGrant.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,eAAO,MAAM,oBAAoB,4BAA6B,CAAA;AAC9D,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAA;AAErE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,CAAC,EAAE,CAAC,CAAA;IACJ,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAA;IACd,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,iBAAiB,EAAE,CAAA;IAC3B,oDAAoD;IACpD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,oBAAoB;IACpB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,oBAAoB;IACpB,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAKD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAAI,SAAS,OAAO,KAAG,kBAAkB,GAAG,IAyB/E,CAAA;AAED,oFAAoF;AACpF,eAAO,MAAM,2BAA2B,GAAI,QAAQ,MAAM,KAAG,MAG5D,CAAA;AASD;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,KAAG,kBAAkB,GAAG,IAQvE,CAAA"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The Transloadit Storage grant: a short-lived HS256 JWT an integrator's
3
+ * server mints (or api2's `POST /storage/grants` mints for it) and
4
+ * Companion's S3 provider verifies. This module is the one wire contract —
5
+ * claim shape, decoding, and (in `@transloadit/utils/node`) minting and
6
+ * verification — so the implementations cannot drift apart.
7
+ *
8
+ * Everything in this file is browser-safe: the client may *read* a grant's
9
+ * claims to decide what UI to show, but verification is the server's job.
10
+ */
11
+ export const STORAGE_GRANT_SCOPES = ['read', 'write'];
12
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
13
+ /**
14
+ * Validates a decoded JWT payload against the grant contract. Returns the
15
+ * claims (scopes deduplicated) or null when the shape is not a v1 grant.
16
+ */
17
+ export const parseStorageGrantClaims = (payload) => {
18
+ if (!isRecord(payload) ||
19
+ payload.v !== 1 ||
20
+ typeof payload.bucket !== 'string' ||
21
+ payload.bucket.length === 0 ||
22
+ typeof payload.prefix !== 'string' ||
23
+ !Array.isArray(payload.scopes) ||
24
+ !payload.scopes.every((scope) => typeof scope === 'string' && STORAGE_GRANT_SCOPES.includes(scope)) ||
25
+ typeof payload.exp !== 'number') {
26
+ return null;
27
+ }
28
+ return {
29
+ v: 1,
30
+ bucket: payload.bucket,
31
+ prefix: payload.prefix,
32
+ scopes: [...new Set(payload.scopes)],
33
+ ...(typeof payload.sub === 'string' && { sub: payload.sub }),
34
+ ...(typeof payload.iat === 'number' && { iat: payload.iat }),
35
+ exp: payload.exp,
36
+ };
37
+ };
38
+ /** Companion's prefix policy: no leading slashes, a trailing slash unless empty. */
39
+ export const normalizeStorageGrantPrefix = (prefix) => {
40
+ const cleaned = prefix.replace(/^\/+/, '');
41
+ return cleaned.length === 0 || cleaned.endsWith('/') ? cleaned : `${cleaned}/`;
42
+ };
43
+ const decodeBase64UrlToUtf8 = (input) => {
44
+ const base64 = input.replace(/-/g, '+').replace(/_/g, '/');
45
+ const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
46
+ const binary = atob(padded);
47
+ return new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));
48
+ };
49
+ /**
50
+ * Reads a grant's payload without verifying the signature — for clients that
51
+ * only need to know what the session will be allowed to do. Returns null for
52
+ * anything that does not decode to a v1 grant.
53
+ */
54
+ export const decodeStorageGrant = (token) => {
55
+ try {
56
+ const payload = token.split('.')[1];
57
+ if (payload === undefined || payload.length === 0)
58
+ return null;
59
+ return parseStorageGrantClaims(JSON.parse(decodeBase64UrlToUtf8(payload)));
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transloadit/utils",
3
- "version": "4.7.0",
3
+ "version": "4.8.0",
4
4
  "description": "Transloadit shared utilities",
5
5
  "type": "module",
6
6
  "license": "MIT",