Skip to content

Commit 2cd545d

Browse files
committed
feat: support signed urls
1 parent 00af0ca commit 2cd545d

File tree

8 files changed

+984
-1334
lines changed

8 files changed

+984
-1334
lines changed

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
"import": "./lib/index.js",
1010
"default": "./lib/index.js"
1111
},
12+
"./signed": {
13+
"source": "./src/signed/index.ts",
14+
"import": "./lib/signed/index.js",
15+
"default": "./lib/signed/index.js"
16+
},
1217
"./package.json": "./package.json"
1318
},
1419
"main": "./lib/index.js",
@@ -79,5 +84,8 @@
7984
"content",
8085
"image-url"
8186
],
87+
"dependencies": {
88+
"@sanity/signed-urls": "^2.0.0"
89+
},
8290
"packageManager": "[email protected]"
8391
}

pnpm-lock.yaml

Lines changed: 652 additions & 1317 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/builder.ts

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ function isSanityClientLike(
2929
return client && 'clientConfig' in client ? typeof client.clientConfig === 'object' : false
3030
}
3131

32-
function rewriteSpecName(key: string) {
32+
/**
33+
* @internal
34+
*/
35+
export function rewriteSpecName(key: string) {
3336
const specs = SPEC_NAME_TO_URL_NAME_MAPPINGS
3437
for (const entry of specs) {
3538
const [specName, param] = entry
@@ -42,37 +45,52 @@ function rewriteSpecName(key: string) {
4245
}
4346

4447
/**
45-
* @public
48+
* @internal
4649
*/
47-
export function createImageUrlBuilder(
48-
options?: SanityClientLike | SanityProjectDetails | SanityModernClientLike
49-
) {
50-
// Did we get a modernish client?
51-
if (isSanityModernClientLike(options)) {
50+
export function createBuilder<C extends typeof ImageUrlBuilder>(
51+
Builder: C,
52+
_options?: SanityClientLike | SanityProjectDetails | SanityModernClientLike
53+
): InstanceType<C> {
54+
let options: ConstructorParameters<C>[1] = {}
55+
56+
if (isSanityModernClientLike(_options)) {
5257
// Inherit config from client
53-
const {apiHost: apiUrl, projectId, dataset} = options.config()
58+
const {apiHost: apiUrl, projectId, dataset} = _options.config()
5459
const apiHost = apiUrl || 'https://api.sanity.io'
55-
return new ImageUrlBuilder(null, {
60+
options = {
5661
baseUrl: apiHost.replace(/^https:\/\/api\./, 'https://cdn.'),
5762
projectId,
5863
dataset,
59-
})
64+
}
6065
}
6166

6267
// Did we get a SanityClient?
63-
if (isSanityClientLike(options)) {
68+
else if (isSanityClientLike(_options)) {
6469
// Inherit config from client
65-
const {apiHost: apiUrl, projectId, dataset} = options.clientConfig
70+
const {apiHost: apiUrl, projectId, dataset} = _options.clientConfig
6671
const apiHost = apiUrl || 'https://api.sanity.io'
67-
return new ImageUrlBuilder(null, {
72+
options = {
6873
baseUrl: apiHost.replace(/^https:\/\/api\./, 'https://cdn.'),
6974
projectId,
7075
dataset,
71-
})
76+
}
7277
}
7378

7479
// Or just accept the options as given
75-
return new ImageUrlBuilder(null, options || {})
80+
else {
81+
options = _options || {}
82+
}
83+
84+
return new Builder(null, options) as InstanceType<C>
85+
}
86+
87+
/**
88+
* @public
89+
*/
90+
export function createImageUrlBuilder(
91+
options?: SanityClientLike | SanityProjectDetails | SanityModernClientLike
92+
) {
93+
return createBuilder(ImageUrlBuilder, options)
7694
}
7795

7896
/**
@@ -87,7 +105,7 @@ export class ImageUrlBuilder {
87105
: {...(options || {})} // Copy options
88106
}
89107

90-
withOptions(options: Partial<ImageUrlBuilderOptionsWithAliases>) {
108+
protected constructNewOptions(options: Partial<ImageUrlBuilderOptionsWithAliases>) {
91109
const baseUrl = options.baseUrl || this.options.baseUrl
92110

93111
const newOptions: {[key: string]: any} = {baseUrl}
@@ -97,8 +115,12 @@ export class ImageUrlBuilder {
97115
newOptions[specKey] = options[key]
98116
}
99117
}
118+
return {baseUrl, ...newOptions}
119+
}
100120

101-
return new ImageUrlBuilder(this, {baseUrl, ...newOptions})
121+
withOptions(options: Partial<ImageUrlBuilderOptionsWithAliases>): this {
122+
const newOptions = this.constructNewOptions(options)
123+
return new ImageUrlBuilder(this, newOptions) as this
102124
}
103125

104126
// The image to be represented. Accepts a Sanity 'image'-document, 'asset'-document or

src/signed/index.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export {createImageUrlBuilder} from './signed-builder'
2+
3+
export type {
4+
AutoMode,
5+
CropMode,
6+
CropSpec,
7+
FitMode,
8+
HotspotSpec,
9+
ImageFormat,
10+
ImageUrlBuilderOptions,
11+
ImageUrlBuilderOptionsWithAliases,
12+
ImageUrlBuilderOptionsWithAsset,
13+
Orientation,
14+
SanityAsset,
15+
SanityClientLike,
16+
SanityImageCrop,
17+
SanityImageDimensions,
18+
SanityImageFitResult,
19+
SanityImageHotspot,
20+
SanityImageObject,
21+
SanityImageRect,
22+
SanityImageSource,
23+
SanityImageWithAssetStub,
24+
SanityModernClientLike,
25+
SanityProjectDetails,
26+
SanityReference,
27+
} from '../types'
28+
29+
export type {ImageUrlSigningOptions, ImageUrlSignedBuilderOptions} from './types'

src/signed/signed-builder.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {createBuilder, ImageUrlBuilder} from '../builder'
2+
import {signedUrlForImage} from './signedUrlForImage'
3+
import type {ImageUrlSignedBuilderOptions, ImageUrlSigningOptions} from './types'
4+
import type {
5+
ImageUrlBuilderOptions,
6+
ImageUrlBuilderOptionsWithAliases,
7+
SanityClientLike,
8+
SanityModernClientLike,
9+
SanityProjectDetails,
10+
} from '../types'
11+
12+
function assertValidSignedOptions(
13+
opts: Partial<ImageUrlSigningOptions>
14+
): asserts opts is ImageUrlSigningOptions {
15+
if (typeof opts.keyId !== 'string') {
16+
throw new Error('Cannot call `signedUrl()` without `keyId`')
17+
}
18+
19+
if (typeof opts.privateKey !== 'string') {
20+
throw new Error('Cannot call `signedUrl()` without `privateKey`')
21+
}
22+
}
23+
24+
/**
25+
* @internal
26+
*/
27+
export class ImageSignedUrlBuilder extends ImageUrlBuilder {
28+
public declare options: ImageUrlBuilderOptions & Partial<ImageUrlSigningOptions>
29+
30+
constructor(parent: ImageSignedUrlBuilder | null, options: ImageUrlSignedBuilderOptions) {
31+
super(parent, options)
32+
}
33+
34+
override withOptions(
35+
options: Partial<ImageUrlBuilderOptionsWithAliases & ImageUrlSigningOptions>
36+
): this {
37+
const newOptions = this.constructNewOptions(options)
38+
return new ImageSignedUrlBuilder(this, {...newOptions}) as this
39+
}
40+
41+
expiry(expiry: string | Date) {
42+
return this.withOptions({expiry})
43+
}
44+
45+
signingKey(keyId: string, privateKey: string) {
46+
return this.withOptions({keyId, privateKey})
47+
}
48+
49+
signedUrl() {
50+
const {expiry, keyId, privateKey, ...rest} = this.options
51+
const signedOptions = {expiry, keyId, privateKey}
52+
assertValidSignedOptions(signedOptions)
53+
return signedUrlForImage(rest, signedOptions)
54+
}
55+
}
56+
57+
/**
58+
* @public
59+
*/
60+
export function createImageUrlBuilder(
61+
options?: SanityClientLike | SanityProjectDetails | SanityModernClientLike
62+
) {
63+
return createBuilder(ImageSignedUrlBuilder, options)
64+
}

src/signed/signedUrlForImage.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {signUrl} from '@sanity/signed-urls'
2+
import type {ImageUrlBuilderOptions} from '../types'
3+
import type {ImageUrlSigningOptions} from './types'
4+
import {urlForImage} from '../urlForImage'
5+
6+
export function signedUrlForImage(
7+
options: ImageUrlBuilderOptions,
8+
signingOptions: ImageUrlSigningOptions
9+
): string {
10+
// Get the base URL without any signing specific parameters
11+
const baseUrl = urlForImage(options)
12+
// Sign the URL with the signing parameters
13+
return signUrl(baseUrl, signingOptions)
14+
}

src/signed/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type {ImageUrlBuilderOptions} from '../types'
2+
3+
/**
4+
* @public
5+
*/
6+
export interface ImageUrlSigningOptions {
7+
keyId: string
8+
privateKey: string
9+
expiry?: string | Date
10+
}
11+
12+
/**
13+
* @public
14+
*/
15+
export type ImageUrlSignedBuilderOptions = ImageUrlBuilderOptions & Partial<ImageUrlSigningOptions>

0 commit comments

Comments
 (0)