diff --git a/examples/aws-static-site-basic-auth/sst.config.ts b/examples/aws-static-site-basic-auth/sst.config.ts index adffbc112..7b2a141d7 100644 --- a/examples/aws-static-site-basic-auth/sst.config.ts +++ b/examples/aws-static-site-basic-auth/sst.config.ts @@ -46,28 +46,21 @@ export default $config({ Buffer.from(`${username}:${password}`).toString("base64") ); - const fn = new aws.cloudfront.Function("BasicAuth", { - runtime: "cloudfront-js-2.0", - code: $interpolate` - function handler(event) { - if (!event.request.headers.authorization || event.request.headers.authorization.value !== "Basic ${basicAuth}") { - return { - statusCode: 401, - headers: { - "www-authenticate": { value: "Basic" } - } - }; - } - return event.request; - }`, - }); - new sst.aws.StaticSite("MySite", { path: "site", - // Don't password protect prod - edge: $app.stage !== "production" - ? { viewerRequest: fn.arn } - : undefined, + edge: { + viewerRequest: { + injection: $interpolate` + if (!event.request.headers.authorization || event.request.headers.authorization.value !== "Basic ${basicAuth}") { + return { + statusCode: 401, + headers: { + "www-authenticate": { value: "Basic" } + } + }; + }`, + }, + }, }); }, }); diff --git a/platform/src/components/aws/static-site.ts b/platform/src/components/aws/static-site.ts index 9e3a9304a..a6aefe885 100644 --- a/platform/src/components/aws/static-site.ts +++ b/platform/src/components/aws/static-site.ts @@ -71,41 +71,103 @@ export interface StaticSiteArgs extends BaseStaticSiteArgs { */ edge?: Input<{ /** - * The ARN of a CloudFront function to modify the incoming request before it reaches your origin server. + * Configure the viewer request function. * - * For example, you can use this to redirect users, rewrite URLs, or add headers. - * - * By default, a viewer request function is created to rewrite URLs to: - * - append `index.html` to the URL if the URL ends with a `/`. - * - append `.html` to the URL if the URL does not contain a file extension. - * - * @default Uses the default viewer request function. - * @example - * ```js - * { - * edge: { - * viewerRequest: "arn:aws:cloudfront::1234567890:function/MyViewRequestFunction" - * } - * } - * ``` + * The viewer request function can be used to modify incoming requests before they reach + * your origin server. For example, you can redirect users, rewrite URLs, or add headers. */ - viewerRequest?: Input; + viewerRequest?: Input<{ + /** + * Inject your code into the viewer request function. + * + * By default, a viewer request function is created to rewrite URLs to: + * - append `index.html` to the URL if the URL ends with a `/`. + * - append `.html` to the URL if the URL does not contain a file extension. + * + * The provided code will be injected at the end of the function. + * + * ```js + * function handler(event) { + * + * // Default behavior code + * ... + * + * // User injected code + * ... + * + * return event.request; + * } + * ``` + * + * @example + * Add a custom header to all requests + * ```js + * { + * edge: { + * viewerRequest: { + * injection: `event.request.headers["x-foo"] = "bar";` + * } + * } + * } + * ``` + */ + injection: Input; + /** + * The KV stores to associate with the viewer request function. + * + * @example + * ```js + * { + * edge: { + * viewerRequest: { + * kvStores: ["arn:aws:cloudfront::123456789012:key-value-store/my-store"] + * } + * } + * } + * ``` + */ + kvStores?: Input[]>; + }>; /** - * The ARN of the CloudFront function to use for the viewer response. + * Configure the viewer response function. * * The viewer response function can be used to modify outgoing responses before they are sent to the client. For example, you can add security headers or change the response status code. * - * @default No viewer response function is set. + * By default, no viewer response function is set. A new function will be created with the provided code. + * * @example + * Add a custom header to all responses * ```js * { * edge: { - * viewerResponse: "arn:aws:cloudfront::1234567890:function/MyViewResponseFunction" + * viewerResponse: { + * injection: `event.response.headers["x-foo"] = "bar";` + * } * } * } * ``` */ - viewerResponse?: Input; + viewerResponse?: Input<{ + /** + * Code to inject into the viewer response function. + */ + injection: Input; + /** + * The KV stores to associate with the viewer response function. + * + * @example + * ```js + * { + * edge: { + * viewerResponse: { + * kvStores: ["arn:aws:cloudfront::123456789012:key-value-store/my-store"] + * } + * } + * } + * ``` + */ + kvStores?: Input[]>; + }>; }>; /** * Configure if your static site needs to be built. This is useful if you are using a static site generator. @@ -254,46 +316,46 @@ export interface StaticSiteArgs extends BaseStaticSiteArgs { invalidation?: Input< | false | { - /** - * Configure if `sst deploy` should wait for the CloudFront cache invalidation to finish. - * - * :::tip - * For non-prod environments it might make sense to pass in `false`. - * ::: - * - * Waiting for the CloudFront cache invalidation process to finish ensures that the new content will be served once the deploy finishes. However, this process can sometimes take more than 5 mins. - * @default `false` - * @example - * ```js - * { - * invalidation: { - * wait: true - * } - * } - * ``` - */ - wait?: Input; - /** - * The paths to invalidate. - * - * You can either pass in an array of glob patterns to invalidate specific files. Or you can use the built-in option `all` to invalidation all files when any file changes. - * - * :::note - * Invalidating `all` counts as one invalidation, while each glob pattern counts as a single invalidation path. - * ::: - * @default `"all"` - * @example - * Invalidate the `index.html` and all files under the `products/` route. - * ```js - * { - * invalidation: { - * paths: ["/index.html", "/products/*"] - * } - * } - * ``` - */ - paths?: Input<"all" | string[]>; - } + /** + * Configure if `sst deploy` should wait for the CloudFront cache invalidation to finish. + * + * :::tip + * For non-prod environments it might make sense to pass in `false`. + * ::: + * + * Waiting for the CloudFront cache invalidation process to finish ensures that the new content will be served once the deploy finishes. However, this process can sometimes take more than 5 mins. + * @default `false` + * @example + * ```js + * { + * invalidation: { + * wait: true + * } + * } + * ``` + */ + wait?: Input; + /** + * The paths to invalidate. + * + * You can either pass in an array of glob patterns to invalidate specific files. Or you can use the built-in option `all` to invalidation all files when any file changes. + * + * :::note + * Invalidating `all` counts as one invalidation, while each glob pattern counts as a single invalidation path. + * ::: + * @default `"all"` + * @example + * Invalidate the `index.html` and all files under the `products/` route. + * ```js + * { + * invalidation: { + * paths: ["/index.html", "/products/*"] + * } + * } + * ``` + */ + paths?: Input<"all" | string[]>; + } >; /** * [Transform](/docs/components#transform) how this component creates its underlying @@ -467,7 +529,6 @@ export class StaticSite extends Component implements Link.Linkable { ) { super(__pulumiType, name, args, opts); - let defaultCfFunction: cloudfront.Function; const parent = this; const { sitePath, environment, indexPage } = prepare(args); const dev = normalizeDev(); @@ -550,8 +611,8 @@ export class StaticSite extends Component implements Link.Linkable { ...args.assets, path: args.assets?.path ? output(args.assets?.path).apply((v) => - v.replace(/^\//, "").replace(/\/$/, ""), - ) + v.replace(/^\//, "").replace(/\/$/, ""), + ) : undefined, }; } @@ -609,8 +670,8 @@ export class StaticSite extends Component implements Link.Linkable { const s3Bucket = bucket ? bucket.nodes.bucket : s3.BucketV2.get(`${name}Assets`, assets.bucket!, undefined, { - parent, - }); + parent, + }); return { bucketName: s3Bucket.bucket, @@ -745,29 +806,29 @@ export class StaticSite extends Component implements Link.Linkable { defaultRootObject: indexPage, customErrorResponses: args.errorPage ? [ - { - errorCode: 403, - responsePagePath: interpolate`/${args.errorPage}`, - responseCode: 403, - }, - { - errorCode: 404, - responsePagePath: interpolate`/${args.errorPage}`, - responseCode: 404, - }, - ] + { + errorCode: 403, + responsePagePath: interpolate`/${args.errorPage}`, + responseCode: 403, + }, + { + errorCode: 404, + responsePagePath: interpolate`/${args.errorPage}`, + responseCode: 404, + }, + ] : [ - { - errorCode: 403, - responsePagePath: interpolate`/${indexPage}`, - responseCode: 200, - }, - { - errorCode: 404, - responsePagePath: interpolate`/${indexPage}`, - responseCode: 200, - }, - ], + { + errorCode: 403, + responsePagePath: interpolate`/${indexPage}`, + responseCode: 200, + }, + { + errorCode: 404, + responsePagePath: interpolate`/${indexPage}`, + responseCode: 200, + }, + ], defaultCacheBehavior: { targetOriginId: "s3", viewerProtocolPolicy: "redirect-to-https", @@ -779,16 +840,15 @@ export class StaticSite extends Component implements Link.Linkable { functionAssociations: output(args.edge).apply((edge) => [ { eventType: "viewer-request", - functionArn: - edge?.viewerRequest ?? createCloudfrontFunction().arn, + functionArn: createCloudfrontFunction("request").arn, }, ...(edge?.viewerResponse ? [ - { - eventType: "viewer-response", - functionArn: edge.viewerResponse, - }, - ] + { + eventType: "viewer-response", + functionArn: createCloudfrontFunction("response").arn, + }, + ] : []), ]), }, @@ -801,29 +861,48 @@ export class StaticSite extends Component implements Link.Linkable { ); } - function createCloudfrontFunction() { - defaultCfFunction = - defaultCfFunction ?? - new cloudfront.Function( - `${name}Function`, - { - runtime: "cloudfront-js-1.0", - code: ` - function handler(event) { - var request = event.request; - var uri = request.uri; - if (uri.endsWith('/')) { - request.uri += 'index.html'; - } else if (!uri.includes('.')) { - request.uri += '.html'; - } - return request; - }`, - }, - { parent }, - ); - - return defaultCfFunction; + function createCloudfrontFunction(type: "request" | "response") { + return type === "request" + ? new cloudfront.Function( + `${name}Function`, + { + runtime: "cloudfront-js-2.0", + keyValueStoreAssociations: output(args.edge).apply( + (edge) => edge?.viewerRequest?.kvStores ?? [], + ), + code: output(args.edge).apply( + (edge) => ` +function handler(event) { + if (event.request.uri.endsWith('/')) { + event.request.uri += 'index.html'; + } else if (!event.request.uri.includes('.')) { + event.request.uri += '.html'; + } + ${edge?.viewerRequest?.injection ?? ""} + return event.request; +}`, + ), + }, + { parent }, + ) + : new cloudfront.Function( + `${name}ResponseFunction`, + { + runtime: "cloudfront-js-2.0", + keyValueStoreAssociations: output(args.edge).apply( + (edge) => edge?.viewerResponse?.kvStores ?? [], + ), + code: output(args.edge).apply( + (edge) => ` +function handler(event) { + ${edge?.viewerResponse?.injection ?? ""} + return event.response; +} +`, + ), + }, + { parent }, + ); } function buildInvalidation() {