@kensio/yulin 1.20.15 → 1.20.16

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.
Files changed (95) hide show
  1. package/README.md +13 -0
  2. package/dist/sdk/module/sim-sdk-module-client-interceptor.d.ts +5 -0
  3. package/dist/sdk/module/sim-sdk-module-client-interceptor.js +14 -0
  4. package/dist/sdk/module/sim-sdk-static-client-factory.d.ts +26 -0
  5. package/dist/sdk/module/sim-sdk-static-client-factory.js +59 -0
  6. package/dist/sdk/send-patch.d.ts +8 -0
  7. package/dist/sdk/send-patch.js +11 -0
  8. package/dist/service/aws/caller/sim-aws-caller-resolver.d.ts +28 -6
  9. package/dist/service/aws/caller/sim-aws-caller-resolver.js +18 -11
  10. package/dist/service/aws/caller/sim-aws-caller.d.ts +9 -0
  11. package/dist/service/aws/caller/sim-aws-resolved-caller.d.ts +19 -0
  12. package/dist/service/aws/caller/sim-aws-resolved-caller.js +26 -0
  13. package/dist/service/aws/factory/sim-aws-account-region-service-builder.d.ts +6 -0
  14. package/dist/service/aws/factory/sim-aws-account-region-service-builder.js +3 -0
  15. package/dist/service/aws/factory/sim-aws-account-service-cache.d.ts +6 -0
  16. package/dist/service/aws/factory/sim-aws-account-service-cache.js +5 -0
  17. package/dist/service/aws/factory/sim-aws-service-factory.d.ts +5 -0
  18. package/dist/service/aws/factory/sim-aws-service-factory.js +6 -0
  19. package/dist/service/aws/sim-aws-properties.d.ts +15 -0
  20. package/dist/service/aws/sim-aws.js +2 -1
  21. package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.d.ts +30 -7
  22. package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.js +20 -11
  23. package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.d.ts +2 -1
  24. package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.js +5 -1
  25. package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.d.ts +10 -0
  26. package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.js +15 -3
  27. package/dist/service/iam/authorize/sim-iam-account-auth-z.d.ts +11 -4
  28. package/dist/service/iam/authorize/sim-iam-account-auth-z.js +5 -3
  29. package/dist/service/iam/authorize/sim-iam-authorizer.d.ts +2 -1
  30. package/dist/service/iam/authorize/sim-iam-authorizer.js +1 -0
  31. package/dist/service/iam/role/sim-iam-role-with-policy.factory.d.ts +9 -0
  32. package/dist/service/iam/role/sim-iam-role-with-policy.factory.js +4 -2
  33. package/dist/service/iam/sim-iam-account-parts.d.ts +8 -0
  34. package/dist/service/iam/sim-iam-account-parts.js +1 -0
  35. package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.d.ts +9 -2
  36. package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.js +2 -2
  37. package/dist/service/sts/auth-z/assume-role-source-account-auth-z.d.ts +10 -4
  38. package/dist/service/sts/auth-z/assume-role-source-account-auth-z.js +8 -7
  39. package/dist/service/sts/auth-z/assume-role-target-auth-z.d.ts +2 -2
  40. package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.d.ts +6 -2
  41. package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.js +14 -11
  42. package/dist/service/sts/command/assume-role/assume-role.handler.d.ts +5 -1
  43. package/dist/service/sts/command/assume-role/assume-role.handler.js +5 -2
  44. package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.d.ts +5 -1
  45. package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.js +4 -1
  46. package/dist/service/sts/service-role/sim-service-role.js +5 -1
  47. package/dist/service/sts/sim-sts.d.ts +9 -1
  48. package/dist/service/sts/sim-sts.js +4 -0
  49. package/docs/README.md +54 -0
  50. package/docs/ai-skill/README.md +69 -0
  51. package/docs/cli/README.md +284 -0
  52. package/docs/factories/README.md +140 -0
  53. package/docs/lint/README.md +176 -0
  54. package/docs/non-aws-dependencies/README.md +261 -0
  55. package/docs/sdk/README.md +272 -0
  56. package/docs/serve/README.md +1126 -0
  57. package/docs/services/acm/README.md +823 -0
  58. package/docs/services/apigateway/README.md +1919 -0
  59. package/docs/services/apigatewayv2/README.md +2678 -0
  60. package/docs/services/athena/README.md +1044 -0
  61. package/docs/services/bedrock/README.md +350 -0
  62. package/docs/services/cloudformation/README.md +3440 -0
  63. package/docs/services/cloudfront/README.md +2874 -0
  64. package/docs/services/cloudwatch/README.md +420 -0
  65. package/docs/services/cognito/README.md +4923 -0
  66. package/docs/services/dynamodb/README.md +3784 -0
  67. package/docs/services/ecr/README.md +302 -0
  68. package/docs/services/ecs/README.md +2010 -0
  69. package/docs/services/elbv2/README.md +1897 -0
  70. package/docs/services/eventbridge/README.md +1110 -0
  71. package/docs/services/firehose/README.md +805 -0
  72. package/docs/services/glue/README.md +501 -0
  73. package/docs/services/iam/README.md +1283 -0
  74. package/docs/services/kinesis/README.md +454 -0
  75. package/docs/services/kms/README.md +659 -0
  76. package/docs/services/lambda/README.md +3645 -0
  77. package/docs/services/logs/README.md +868 -0
  78. package/docs/services/organizations/README.md +574 -0
  79. package/docs/services/personalize/README.md +835 -0
  80. package/docs/services/rekognition/README.md +1070 -0
  81. package/docs/services/route53/README.md +1783 -0
  82. package/docs/services/s3/README.md +3024 -0
  83. package/docs/services/scheduler/README.md +806 -0
  84. package/docs/services/secretsmanager/README.md +667 -0
  85. package/docs/services/ses/README.md +1011 -0
  86. package/docs/services/sns/README.md +1904 -0
  87. package/docs/services/sqs/README.md +1185 -0
  88. package/docs/services/ssm/README.md +1072 -0
  89. package/docs/services/stepfunctions/README.md +1332 -0
  90. package/docs/services/sts/README.md +245 -0
  91. package/docs/services/wafv2/README.md +1378 -0
  92. package/docs/terraform/README.md +248 -0
  93. package/docs/time/README.md +284 -0
  94. package/llms.txt +59 -0
  95. package/package.json +7 -4
@@ -0,0 +1,2874 @@
1
+ # Simulated CloudFront
2
+
3
+ Yulin includes a simulated CloudFront service for tests and local development.
4
+
5
+ Sim CloudFront can be used directly through `SimAws`, and it can also be served on localhost
6
+ alongside other simulated AWS services, so application code can make HTTP requests through a
7
+ CloudFront-like layer without talking to real AWS.
8
+
9
+ `SimCloudFront` can also be instantiated on its own, in which case it has its own isolated state,
10
+ standing apart from any wider simulated AWS environment.
11
+
12
+ ## Basic Distribution setup
13
+
14
+ Create a simulated AWS environment, add a sim S3 Bucket, and create a sim CloudFront Distribution
15
+ pointing at that Bucket.
16
+
17
+ ```typescript sim-cloudfront-distribution-s3-origin
18
+ /**
19
+ * Creating a simulated CloudFront Distribution with a simulated S3 Origin.
20
+ */
21
+
22
+ import { CreateDistributionCommand } from "@aws-sdk/client-cloudfront";
23
+ import {
24
+ CreateBucketCommand,
25
+ PutBucketPolicyCommand,
26
+ PutPublicAccessBlockCommand,
27
+ } from "@aws-sdk/client-s3";
28
+
29
+ import { SimAws } from "@kensio/yulin";
30
+
31
+ const simAws = new SimAws();
32
+ const simS3 = simAws.s3();
33
+ const simCloudFront = simAws.cloudFront();
34
+
35
+ await simS3.createBucket(
36
+ new CreateBucketCommand({
37
+ Bucket: "foo-bucket",
38
+ }),
39
+ );
40
+
41
+ // The Origin below has no origin access control, so it reads the Bucket
42
+ // anonymously and only a public read grant lets it serve anything.
43
+ await simS3.putPublicAccessBlock(
44
+ new PutPublicAccessBlockCommand({
45
+ Bucket: "foo-bucket",
46
+ PublicAccessBlockConfiguration: {
47
+ BlockPublicAcls: true,
48
+ IgnorePublicAcls: true,
49
+ },
50
+ }),
51
+ );
52
+ await simS3.putBucketPolicy(
53
+ new PutBucketPolicyCommand({
54
+ Bucket: "foo-bucket",
55
+ Policy: JSON.stringify({
56
+ Version: "2012-10-17",
57
+ Statement: {
58
+ Effect: "Allow",
59
+ Principal: "*",
60
+ Action: "s3:GetObject",
61
+ Resource: "arn:aws:s3:::foo-bucket/*",
62
+ },
63
+ }),
64
+ }),
65
+ );
66
+
67
+ const distributionCreation = await simCloudFront.createDistribution(
68
+ new CreateDistributionCommand({
69
+ DistributionConfig: {
70
+ CallerReference: "assets-cdn",
71
+ Comment: "Assets CDN",
72
+ Enabled: true,
73
+ Origins: {
74
+ Quantity: 1,
75
+ Items: [
76
+ {
77
+ Id: "assets-origin",
78
+ DomainName: "foo-bucket.s3.amazonaws.com",
79
+ S3OriginConfig: {
80
+ OriginAccessIdentity: "",
81
+ },
82
+ },
83
+ ],
84
+ },
85
+ DefaultCacheBehavior: {
86
+ TargetOriginId: "assets-origin",
87
+ ViewerProtocolPolicy: "allow-all",
88
+ },
89
+ },
90
+ }),
91
+ );
92
+
93
+ console.log(distributionCreation.Distribution?.DomainName);
94
+ ```
95
+
96
+ ## What an S3 Origin can read
97
+
98
+ An S3 Origin reads its Bucket through the ordinary GetObject command. The Bucket policy decides what
99
+ the Distribution can serve. An Origin with no origin access control reads anonymously, the unsigned
100
+ request real CloudFront sends to the S3 REST endpoint. An Object has to be publicly readable for the
101
+ Distribution to serve it, and a Bucket with no policy answers 403 for every Object.
102
+
103
+ An Origin that does have an origin access control reads as the CloudFront service principal. The
104
+ Bucket stays private and its policy names the Distribution. See
105
+ [Origin access controls](#origin-access-controls) for the Bucket policy that takes.
106
+
107
+ That is what the two commands in the example above do. `PutPublicAccessBlockCommand` opts out of the
108
+ block on public Bucket policies, then `PutBucketPolicyCommand` grants `s3:GetObject` to
109
+ `Principal: "*"`. The same pair is what a static website Bucket needs, and it is what CDK's
110
+ `publicReadAccess: true` generates.
111
+
112
+ A denied read reaches the viewer as a 403 from the Origin, and a Distribution's custom error
113
+ response for 403 replaces it. The usual single-page-app setup, rewriting 403 to `/index.html`,
114
+ behaves here as it does in AWS.
115
+
116
+ `S3OriginConfig.OriginAccessIdentity` is refused. Leave it empty, as CloudFront itself writes it for
117
+ an Origin that signs nothing.
118
+
119
+ ## Static sites, default root objects and error pages
120
+
121
+ A static site behind CloudFront usually leans on two Distribution settings. `DefaultRootObject`
122
+ makes a request for the site root return the home page. `CustomErrorResponses` makes a URL that
123
+ matches no object return the site's own error page in place of the Origin's. Sim CloudFront applies
124
+ both, and a test can assert what a visitor would actually see.
125
+
126
+ ```typescript sim-cloudfront-static-site
127
+ /**
128
+ * Serving a static site with a default root object and a custom error page.
129
+ */
130
+
131
+ import { CreateDistributionCommand } from "@aws-sdk/client-cloudfront";
132
+ import {
133
+ CreateBucketCommand,
134
+ PutBucketPolicyCommand,
135
+ PutObjectCommand,
136
+ PutPublicAccessBlockCommand,
137
+ } from "@aws-sdk/client-s3";
138
+
139
+ import { SimAws } from "@kensio/yulin";
140
+ import { serveSimAws } from "@kensio/yulin/serve";
141
+
142
+ const simAws = new SimAws();
143
+ const srv = await serveSimAws({ simAws });
144
+
145
+ try {
146
+ const simS3 = simAws.s3();
147
+
148
+ await simS3.createBucket(new CreateBucketCommand({ Bucket: "site-bucket" }));
149
+
150
+ // A CloudFront S3 Origin with no origin access control reads the Bucket
151
+ // anonymously, so what it serves has to be publicly readable.
152
+ await simS3.putPublicAccessBlock(
153
+ new PutPublicAccessBlockCommand({
154
+ Bucket: "site-bucket",
155
+ PublicAccessBlockConfiguration: {
156
+ BlockPublicAcls: true,
157
+ IgnorePublicAcls: true,
158
+ },
159
+ }),
160
+ );
161
+ await simS3.putBucketPolicy(
162
+ new PutBucketPolicyCommand({
163
+ Bucket: "site-bucket",
164
+ Policy: JSON.stringify({
165
+ Version: "2012-10-17",
166
+ Statement: {
167
+ Effect: "Allow",
168
+ Principal: "*",
169
+ Action: "s3:GetObject",
170
+ Resource: "arn:aws:s3:::site-bucket/*",
171
+ },
172
+ }),
173
+ }),
174
+ );
175
+
176
+ const pages = {
177
+ "index.html": "<h1>Home</h1>",
178
+ "404.html": "<h1>Page not found</h1>",
179
+ };
180
+
181
+ for (const [key, body] of Object.entries(pages)) {
182
+ await simS3.putObject(
183
+ new PutObjectCommand({
184
+ Bucket: "site-bucket",
185
+ Key: key,
186
+ ContentType: "text/html",
187
+ Body: body,
188
+ }),
189
+ );
190
+ }
191
+
192
+ const distributionCreation = await simAws.cloudFront().createDistribution(
193
+ new CreateDistributionCommand({
194
+ DistributionConfig: {
195
+ CallerReference: "static-site",
196
+ Comment: "Static site",
197
+ Enabled: true,
198
+ DefaultRootObject: "index.html",
199
+ CustomErrorResponses: {
200
+ Quantity: 2,
201
+ Items: [
202
+ {
203
+ ErrorCode: 404,
204
+ ResponsePagePath: "/404.html",
205
+ ResponseCode: "404",
206
+ },
207
+ {
208
+ ErrorCode: 403,
209
+ ResponsePagePath: "/404.html",
210
+ ResponseCode: "404",
211
+ },
212
+ ],
213
+ },
214
+ Origins: {
215
+ Quantity: 1,
216
+ Items: [
217
+ {
218
+ Id: "site-origin",
219
+ DomainName: "site-bucket.s3.amazonaws.com",
220
+ S3OriginConfig: { OriginAccessIdentity: "" },
221
+ },
222
+ ],
223
+ },
224
+ DefaultCacheBehavior: {
225
+ TargetOriginId: "site-origin",
226
+ ViewerProtocolPolicy: "allow-all",
227
+ },
228
+ },
229
+ }),
230
+ );
231
+
232
+ const distroHostname = distributionCreation.Distribution!.DomainName!;
233
+
234
+ const home = await fetch(srv.localUrl(`http://${distroHostname}/`));
235
+ console.log(await home.text()); // <h1>Home</h1>
236
+
237
+ const missing = await fetch(srv.localUrl(`http://${distroHostname}/nowhere`));
238
+ console.log(missing.status); // 404
239
+ console.log(await missing.text()); // <h1>Page not found</h1>
240
+ } finally {
241
+ await srv.close();
242
+ }
243
+ ```
244
+
245
+ The default root object stands in for a request to the root of the Distribution and nothing else. A
246
+ request for `/blog/` is passed to the Origin as it arrived, even where that folder holds its own
247
+ `index.html`. That is where CloudFront differs from an S3 website index document. The substituted
248
+ path is what the rest of request handling sees, and a Cache Behavior pattern and a `viewer-request`
249
+ CloudFront Function both act on the object being served. The value names an object at the Origin. It may be a
250
+ path such as `public/index.html`, and it must not begin with a forward slash. Sim CloudFront refuses one that does with `InvalidDefaultRootObject`. The alternative would
251
+ be a Distribution that answers its own root with a 403.
252
+
253
+ A custom error response replaces the Origin's response when its status matches `ErrorCode`. The
254
+ codes CloudFront supports are 400, 403, 404, 405, 414, 416, 500, 501, 502, 503 and 504. The response
255
+ page is fetched as a request in its own right, and the Cache Behavior matching `ResponsePagePath`
256
+ chooses which Origin it comes from. Error pages can live somewhere other than the content that
257
+ failed. `ResponseCode` is the status the viewer sees. That is how a single-page app serves its shell
258
+ with a 200 for a URL the Bucket has no object for. It is one of the same error codes or 200, the set
259
+ CloudFront allows. Where the response page is itself missing, the viewer gets the status from
260
+ fetching it, as in CloudFront.
261
+
262
+ A viewer-response function never sees a custom error page. CloudFront runs no viewer-response
263
+ function once the Origin has answered 400 or higher, and simulated CloudFront does the same, for a
264
+ CloudFront Function and a Lambda@Edge function alike. The status the Origin returned is what decides
265
+ that, whatever `ResponseCode` puts in its place. `ErrorCachingMinTTL` is accepted and ignored, along
266
+ with a rule that sets nothing else, since sim CloudFront has no cache to apply it to.
267
+
268
+ ## Serve simulated CloudFront on localhost
269
+
270
+ Use `serveSimAws` when you want to make real HTTP requests to the simulated system on localhost.
271
+
272
+ ```typescript serve-sim-cloudfront-localhost
273
+ /**
274
+ * Serving a simulated CloudFront Distribution on localhost.
275
+ */
276
+
277
+ import { CreateDistributionCommand } from "@aws-sdk/client-cloudfront";
278
+ import {
279
+ CreateBucketCommand,
280
+ PutBucketPolicyCommand,
281
+ PutObjectCommand,
282
+ PutPublicAccessBlockCommand,
283
+ } from "@aws-sdk/client-s3";
284
+
285
+ import { SimAws } from "@kensio/yulin";
286
+ import { serveSimAws } from "@kensio/yulin/serve";
287
+
288
+ const simAws = new SimAws();
289
+ const srv = await serveSimAws({ simAws });
290
+
291
+ try {
292
+ const simS3 = simAws.s3();
293
+ const simCloudFront = simAws.cloudFront();
294
+
295
+ await simS3.createBucket(
296
+ new CreateBucketCommand({
297
+ Bucket: "foo-bucket",
298
+ }),
299
+ );
300
+
301
+ // A CloudFront S3 Origin with no origin access control reads the Bucket
302
+ // anonymously, so what it serves has to be publicly readable.
303
+ await simS3.putPublicAccessBlock(
304
+ new PutPublicAccessBlockCommand({
305
+ Bucket: "foo-bucket",
306
+ PublicAccessBlockConfiguration: {
307
+ BlockPublicAcls: true,
308
+ IgnorePublicAcls: true,
309
+ },
310
+ }),
311
+ );
312
+ await simS3.putBucketPolicy(
313
+ new PutBucketPolicyCommand({
314
+ Bucket: "foo-bucket",
315
+ Policy: JSON.stringify({
316
+ Version: "2012-10-17",
317
+ Statement: {
318
+ Effect: "Allow",
319
+ Principal: "*",
320
+ Action: "s3:GetObject",
321
+ Resource: "arn:aws:s3:::foo-bucket/*",
322
+ },
323
+ }),
324
+ }),
325
+ );
326
+
327
+ await simS3.putObject(
328
+ new PutObjectCommand({
329
+ Bucket: "foo-bucket",
330
+ Key: "hello.txt",
331
+ Body: "Hello from simulated CloudFront",
332
+ }),
333
+ );
334
+
335
+ const distributionCreation = await simCloudFront.createDistribution(
336
+ new CreateDistributionCommand({
337
+ DistributionConfig: {
338
+ CallerReference: "localhost-assets-cdn",
339
+ Comment: "Localhost Assets CDN",
340
+ Enabled: true,
341
+ Origins: {
342
+ Quantity: 1,
343
+ Items: [
344
+ {
345
+ Id: "assets-origin",
346
+ DomainName: "foo-bucket.s3.amazonaws.com",
347
+ S3OriginConfig: {
348
+ OriginAccessIdentity: "",
349
+ },
350
+ },
351
+ ],
352
+ },
353
+ DefaultCacheBehavior: {
354
+ TargetOriginId: "assets-origin",
355
+ ViewerProtocolPolicy: "allow-all",
356
+ },
357
+ },
358
+ }),
359
+ );
360
+
361
+ const distroHostname = distributionCreation.Distribution!.DomainName!;
362
+
363
+ const url = srv.localUrl(`http://${distroHostname}/hello.txt`);
364
+ const response = await fetch(url);
365
+
366
+ console.log(response.status);
367
+ console.log(await response.text());
368
+ } finally {
369
+ await srv.close();
370
+ }
371
+ ```
372
+
373
+ The Distribution domain is adapted through `server.localUrl(...)` so that the request is sent to the
374
+ local Yulin server while preserving the simulated CloudFront hostname.
375
+
376
+ A test that needs no browser can skip the port. `SimAwsHttp` answers the same requests in the
377
+ process, with no server listening and no URL to adapt. An alternate domain name a simulated Route53
378
+ answers for is requested by its own name, and `simAwsHttp.fetch("https://cdn.example.test/")` reaches
379
+ the Distribution behind it. See
380
+ [requests without a port](https://yulinsim.dev/serve/#requests-without-a-port "Requests without a port docs").
381
+
382
+ ## Custom Origins
383
+
384
+ An Origin with a `CustomOriginConfig` is one CloudFront reaches over HTTP, in place of reading an S3
385
+ Bucket. Sim CloudFront resolves its `DomainName` in the simulated environment and serves the request
386
+ in process. A Distribution can front a simulated HTTP API endpoint
387
+ (`<api-id>.execute-api.<region>.amazonaws.com`), a simulated Lambda Function URL
388
+ (`<url-id>.lambda-url.<region>.on.aws`), or anything a simulated Route53 record points at one of
389
+ those.
390
+
391
+ That covers the common arrangement of one Distribution serving static assets from a Bucket and
392
+ sending `/api/*` to an API:
393
+
394
+ ```typescript sim-cloudfront-distribution-custom-origin
395
+ /**
396
+ * A simulated CloudFront Distribution fronting a simulated HTTP API.
397
+ */
398
+
399
+ import {
400
+ CreateApiCommand,
401
+ CreateIntegrationCommand,
402
+ CreateRouteCommand,
403
+ CreateStageCommand,
404
+ } from "@aws-sdk/client-apigatewayv2";
405
+ import { CreateDistributionCommand } from "@aws-sdk/client-cloudfront";
406
+ import {
407
+ AddPermissionCommand,
408
+ CreateFunctionCommand,
409
+ } from "@aws-sdk/client-lambda";
410
+ import {
411
+ CreateBucketCommand,
412
+ PutBucketPolicyCommand,
413
+ PutObjectCommand,
414
+ PutPublicAccessBlockCommand,
415
+ } from "@aws-sdk/client-s3";
416
+
417
+ import { SimAws } from "@kensio/yulin";
418
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
419
+ import { serveSimAws } from "@kensio/yulin/serve";
420
+
421
+ const simAws = new SimAws();
422
+
423
+ // A Bucket holding the site, readable by the Origin that reads it anonymously.
424
+ await simAws.s3().createBucket(new CreateBucketCommand({ Bucket: "site" }));
425
+ await simAws.s3().putObject(
426
+ new PutObjectCommand({
427
+ Bucket: "site",
428
+ Key: "index.html",
429
+ Body: "<h1>Site</h1>",
430
+ }),
431
+ );
432
+ await simAws.s3().putPublicAccessBlock(
433
+ new PutPublicAccessBlockCommand({
434
+ Bucket: "site",
435
+ PublicAccessBlockConfiguration: {
436
+ BlockPublicAcls: true,
437
+ IgnorePublicAcls: true,
438
+ },
439
+ }),
440
+ );
441
+ await simAws.s3().putBucketPolicy(
442
+ new PutBucketPolicyCommand({
443
+ Bucket: "site",
444
+ Policy: JSON.stringify({
445
+ Version: "2012-10-17",
446
+ Statement: {
447
+ Effect: "Allow",
448
+ Principal: "*",
449
+ Action: "s3:GetObject",
450
+ Resource: "arn:aws:s3:::site/*",
451
+ },
452
+ }),
453
+ }),
454
+ );
455
+
456
+ // An HTTP API serving /api/things from a function.
457
+ const { FunctionArn } = await simAws.lambda().createFunction(
458
+ new CreateFunctionCommand({
459
+ FunctionName: "things",
460
+ Role: "arn:aws:iam::111111111111:role/ThingsRole",
461
+ Code: { ZipFile: makeLambdaZipFileInput(() => ({ things: ["kettle"] })) },
462
+ }),
463
+ );
464
+
465
+ const apiGateway = simAws.apiGatewayV2();
466
+
467
+ const { ApiId, ApiEndpoint } = await apiGateway.createApi(
468
+ new CreateApiCommand({ Name: "things", ProtocolType: "HTTP" }),
469
+ );
470
+
471
+ const { IntegrationId } = await apiGateway.createIntegration(
472
+ new CreateIntegrationCommand({
473
+ ApiId,
474
+ IntegrationType: "AWS_PROXY",
475
+ IntegrationUri: FunctionArn,
476
+ PayloadFormatVersion: "2.0",
477
+ }),
478
+ );
479
+
480
+ await apiGateway.createRoute(
481
+ new CreateRouteCommand({
482
+ ApiId,
483
+ RouteKey: "GET /api/things",
484
+ Target: `integrations/${IntegrationId}`,
485
+ }),
486
+ );
487
+
488
+ await apiGateway.createStage(
489
+ new CreateStageCommand({ ApiId, StageName: "$default", AutoDeploy: true }),
490
+ );
491
+
492
+ await simAws.lambda().addPermission(
493
+ new AddPermissionCommand({
494
+ FunctionName: "things",
495
+ StatementId: "api-gateway-invoke",
496
+ Action: "lambda:InvokeFunction",
497
+ Principal: "apigateway.amazonaws.com",
498
+ SourceArn: `arn:aws:execute-api:us-east-1:888888888888:${ApiId}/*/*`,
499
+ }),
500
+ );
501
+
502
+ // One Distribution serving the site, with /api/* going to the API.
503
+ const distributionCreation = await simAws.cloudFront().createDistribution(
504
+ new CreateDistributionCommand({
505
+ DistributionConfig: {
506
+ CallerReference: "site-and-api",
507
+ Comment: "Site and API CDN",
508
+ Enabled: true,
509
+ Origins: {
510
+ Quantity: 2,
511
+ Items: [
512
+ {
513
+ Id: "site-origin",
514
+ DomainName: "site.s3.amazonaws.com",
515
+ S3OriginConfig: { OriginAccessIdentity: "" },
516
+ },
517
+ {
518
+ Id: "api-origin",
519
+ DomainName: new URL(ApiEndpoint).hostname,
520
+ CustomOriginConfig: {
521
+ HTTPPort: 80,
522
+ HTTPSPort: 443,
523
+ OriginProtocolPolicy: "https-only",
524
+ },
525
+ },
526
+ ],
527
+ },
528
+ DefaultCacheBehavior: {
529
+ TargetOriginId: "site-origin",
530
+ ViewerProtocolPolicy: "allow-all",
531
+ },
532
+ CacheBehaviors: {
533
+ Quantity: 1,
534
+ Items: [
535
+ {
536
+ PathPattern: "/api/*",
537
+ TargetOriginId: "api-origin",
538
+ ViewerProtocolPolicy: "allow-all",
539
+ },
540
+ ],
541
+ },
542
+ },
543
+ }),
544
+ );
545
+
546
+ const distroHostname = distributionCreation.Distribution!.DomainName!;
547
+ const srv = await serveSimAws({ simAws });
548
+
549
+ try {
550
+ const page = await fetch(srv.localUrl(`http://${distroHostname}/index.html`));
551
+ const things = await fetch(
552
+ srv.localUrl(`http://${distroHostname}/api/things`),
553
+ );
554
+
555
+ console.log(await page.text());
556
+ console.log(await things.text());
557
+ } finally {
558
+ await srv.close();
559
+ }
560
+ ```
561
+
562
+ The Origin domain is resolved when a request is served, and the Distribution and the service behind
563
+ its Origin can be created in either order, whichever way round a CloudFormation template happens to
564
+ declare them.
565
+
566
+ `OriginPath` is prefixed to the request path, as it is for an S3 Origin. An Origin path of `/v1`
567
+ sends a request for `/things` on to `/v1/things`.
568
+
569
+ Three things follow from the request never leaving the process:
570
+
571
+ - A domain unknown to the simulation fails with an error naming the Origin and the
572
+ domain. No real request is made to it, and external HTTP Origins are unsupported.
573
+ - The settings inside `CustomOriginConfig` describe how CloudFront connects over the network. The
574
+ protocol policy, ports, SSL protocols and timeouts are accepted and ignored.
575
+ - The Origin is reached anonymously unless it has an origin access control, as CloudFront reaches an
576
+ Origin it has nothing to sign for. A Function URL or an HTTP API route authorizing with `AWS_IAM`
577
+ therefore refuses the request. [Origin access controls](#origin-access-controls) covers the
578
+ Function URL that admits the Distribution and nothing else.
579
+
580
+ ## Custom headers on an Origin
581
+
582
+ CloudFront adds an Origin's custom headers to every request it sends that Origin. An origin that
583
+ answers only requests carrying a header nothing else knows is how AWS documents
584
+ [restricting a custom origin to CloudFront](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-overview.html#forward-custom-headers-restrict-access),
585
+ and a sim Distribution sends them the same way.
586
+
587
+ The CloudFront API and CloudFormation name the field differently, and both spellings are accepted
588
+ here. The API has `CustomHeaders` inside an `Origin`, and `AWS::CloudFront::Distribution` has
589
+ `OriginCustomHeaders`, as the two differ over the viewer certificate ARN.
590
+
591
+ ```typescript sim-cloudfront-origin-custom-headers
592
+ /**
593
+ * An HTTP API answering only the requests that came through the Distribution.
594
+ */
595
+
596
+ import {
597
+ CreateApiCommand,
598
+ CreateIntegrationCommand,
599
+ CreateRouteCommand,
600
+ CreateStageCommand,
601
+ } from "@aws-sdk/client-apigatewayv2";
602
+ import { CreateDistributionCommand } from "@aws-sdk/client-cloudfront";
603
+ import {
604
+ AddPermissionCommand,
605
+ CreateFunctionCommand,
606
+ } from "@aws-sdk/client-lambda";
607
+
608
+ import { SimAws } from "@kensio/yulin";
609
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
610
+ import { serveSimAws } from "@kensio/yulin/serve";
611
+
612
+ const simAws = new SimAws();
613
+ const originSecret = "5d6e2b0c6f564c1e9d5b2f1a5b8c9d70";
614
+
615
+ // A function serving the API, which reads the secret off every request.
616
+ const { FunctionArn } = await simAws.lambda().createFunction(
617
+ new CreateFunctionCommand({
618
+ FunctionName: "profile",
619
+ Role: "arn:aws:iam::111111111111:role/ProfileRole",
620
+ Code: {
621
+ ZipFile: makeLambdaZipFileInput(
622
+ (event: { headers: Record<string, string> }) =>
623
+ event.headers["x-origin-secret"] === originSecret
624
+ ? { name: "Ada" }
625
+ : { message: "Forbidden" },
626
+ ),
627
+ },
628
+ }),
629
+ );
630
+
631
+ const apiGateway = simAws.apiGatewayV2();
632
+
633
+ const { ApiId, ApiEndpoint } = await apiGateway.createApi(
634
+ new CreateApiCommand({ Name: "profile", ProtocolType: "HTTP" }),
635
+ );
636
+
637
+ const { IntegrationId } = await apiGateway.createIntegration(
638
+ new CreateIntegrationCommand({
639
+ ApiId,
640
+ IntegrationType: "AWS_PROXY",
641
+ IntegrationUri: FunctionArn,
642
+ PayloadFormatVersion: "2.0",
643
+ }),
644
+ );
645
+
646
+ await apiGateway.createRoute(
647
+ new CreateRouteCommand({
648
+ ApiId,
649
+ RouteKey: "GET /user/profile",
650
+ Target: `integrations/${IntegrationId}`,
651
+ }),
652
+ );
653
+
654
+ await apiGateway.createStage(
655
+ new CreateStageCommand({ ApiId, StageName: "$default", AutoDeploy: true }),
656
+ );
657
+
658
+ await simAws.lambda().addPermission(
659
+ new AddPermissionCommand({
660
+ FunctionName: "profile",
661
+ StatementId: "api-gateway-invoke",
662
+ Action: "lambda:InvokeFunction",
663
+ Principal: "apigateway.amazonaws.com",
664
+ SourceArn: `arn:aws:execute-api:us-east-1:888888888888:${ApiId}/*/*`,
665
+ }),
666
+ );
667
+
668
+ // A Distribution that sends the secret with every request to that Origin.
669
+ const distributionCreation = await simAws.cloudFront().createDistribution(
670
+ new CreateDistributionCommand({
671
+ DistributionConfig: {
672
+ CallerReference: "user-site",
673
+ Comment: "User API CDN",
674
+ Enabled: true,
675
+ Origins: {
676
+ Quantity: 1,
677
+ Items: [
678
+ {
679
+ Id: "api-origin",
680
+ DomainName: new URL(ApiEndpoint).hostname,
681
+ CustomOriginConfig: {
682
+ HTTPPort: 80,
683
+ HTTPSPort: 443,
684
+ OriginProtocolPolicy: "https-only",
685
+ },
686
+ CustomHeaders: {
687
+ Quantity: 1,
688
+ Items: [
689
+ { HeaderName: "x-origin-secret", HeaderValue: originSecret },
690
+ ],
691
+ },
692
+ },
693
+ ],
694
+ },
695
+ DefaultCacheBehavior: {
696
+ TargetOriginId: "api-origin",
697
+ ViewerProtocolPolicy: "allow-all",
698
+ },
699
+ },
700
+ }),
701
+ );
702
+
703
+ const distroHostname = distributionCreation.Distribution!.DomainName!;
704
+ const srv = await serveSimAws({ simAws });
705
+
706
+ try {
707
+ const throughCdn = await fetch(
708
+ srv.localUrl(`http://${distroHostname}/user/profile`),
709
+ );
710
+ const direct = await fetch(srv.localUrl(`${ApiEndpoint}/user/profile`));
711
+
712
+ // {"name":"Ada"}
713
+ console.log(await throughCdn.text());
714
+ // {"message":"Forbidden"}
715
+ console.log(await direct.text());
716
+ } finally {
717
+ await srv.close();
718
+ }
719
+ ```
720
+
721
+ Two rules follow CloudFront's own:
722
+
723
+ - A header the viewer already sent is overwritten with the Origin's value, whatever case the viewer
724
+ wrote it in. A viewer cannot reach the origin with a guessed secret by sending the header through
725
+ the Distribution.
726
+ - A header name CloudFront refuses to add fails the Distribution at create and fails the Stack at
727
+ deploy, naming the header. The
728
+ [denied names](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/add-origin-custom-headers.html#add-origin-custom-headers-denylist)
729
+ run from `Cache-Control` to `X-Real-Ip`, along with anything beginning `X-Amz-` or `X-Edge-`.
730
+
731
+ An S3 Origin takes the headers and reaches nothing with them. Sim CloudFront reads a Bucket through
732
+ `GetObject` and builds no HTTP request for a header to travel on, and real S3 ignores a header it
733
+ has no use for.
734
+
735
+ ## An Origin declared twice
736
+
737
+ Two Origins over one domain name are ordinary. They differ by `OriginPath`, by their custom headers
738
+ or by how CloudFront connects, and a Behavior points at whichever one it wants.
739
+
740
+ Two Origins that match in every property but the `Id` are one Origin written twice. Copying a
741
+ Behavior and its Origin together, then editing the path pattern, leaves exactly that behind. Sim
742
+ CloudFront keys an Origin by `Id`, as CloudFront does, and serves both Behaviors alike. An account
743
+ has been seen to serve them differently, refusing every request on the second Behavior at the
744
+ Origin. Why it did that is unconfirmed.
745
+
746
+ The Distribution records each repeat as it is created or updated, and warns about it on the console.
747
+ Each entry in `redundantOrigins` names the Origin, the earlier Origin it repeats and the domain both
748
+ of them name. A test can assert the list is empty.
749
+
750
+ ```typescript sim-cloudfront-redundant-origins
751
+ /**
752
+ * Catching an Origin a Distribution declares twice.
753
+ */
754
+
755
+ import {
756
+ CreateDistributionCommand,
757
+ type Origin,
758
+ } from "@aws-sdk/client-cloudfront";
759
+
760
+ import { SimAws } from "@kensio/yulin";
761
+
762
+ const simCloudFront = new SimAws().cloudFront();
763
+
764
+ // The two Behaviors below were written by copying one of them, so the second
765
+ // Origin says everything the first one says.
766
+ const apiOrigin = (originId: string): Origin => ({
767
+ Id: originId,
768
+ DomainName: "api.example.test",
769
+ CustomOriginConfig: {
770
+ HTTPPort: 80,
771
+ HTTPSPort: 443,
772
+ OriginProtocolPolicy: "https-only",
773
+ },
774
+ CustomHeaders: {
775
+ Quantity: 1,
776
+ Items: [
777
+ {
778
+ HeaderName: "x-origin-secret",
779
+ HeaderValue: "5d6e2b0c6f564c1e9d5b2f1a5b8c9d70",
780
+ },
781
+ ],
782
+ },
783
+ });
784
+
785
+ const creation = await simCloudFront.createDistribution(
786
+ new CreateDistributionCommand({
787
+ DistributionConfig: {
788
+ CallerReference: "user-site",
789
+ Comment: "User API CDN",
790
+ Enabled: true,
791
+ Origins: {
792
+ Quantity: 2,
793
+ Items: [apiOrigin("live-origin"), apiOrigin("preview-origin")],
794
+ },
795
+ DefaultCacheBehavior: {
796
+ TargetOriginId: "live-origin",
797
+ ViewerProtocolPolicy: "allow-all",
798
+ },
799
+ CacheBehaviors: {
800
+ Quantity: 1,
801
+ Items: [
802
+ {
803
+ PathPattern: "/preview/*",
804
+ TargetOriginId: "preview-origin",
805
+ ViewerProtocolPolicy: "allow-all",
806
+ },
807
+ ],
808
+ },
809
+ },
810
+ }),
811
+ );
812
+
813
+ const distribution = simCloudFront.getSimDistributionById(
814
+ creation.Distribution!.Id!,
815
+ );
816
+
817
+ // [
818
+ // {
819
+ // originId: "preview-origin",
820
+ // repeatsOriginId: "live-origin",
821
+ // domainName: "api.example.test",
822
+ // },
823
+ // ]
824
+ console.log(distribution?.redundantOrigins);
825
+ ```
826
+
827
+ Sameness is every property the config declares apart from the `Id`, and not only the properties the
828
+ simulation reads. An Origin differing by a connection setting sim CloudFront ignores is left alone.
829
+ A property written as an empty string counts as one left out, and custom headers count by name and
830
+ value however they were ordered or cased.
831
+
832
+ ## Viewer certificates
833
+
834
+ A Distribution with alternate domain names needs an ACM certificate, and CloudFront accepts only
835
+ certain ones. Sim CloudFront applies the same rules. A Distribution that real CloudFront would
836
+ reject at deploy time is rejected here first, with `InvalidViewerCertificate`:
837
+
838
+ - the certificate must be in `us-east-1`, wherever the rest of your infrastructure lives
839
+ - the certificate must exist and be `ISSUED`
840
+ - every alternate domain name must be covered by the certificate's domain name or one of its subject
841
+ alternative names, with a wildcard covering exactly one label
842
+
843
+ The `us-east-1` rule is easy to miss, because nothing else in a stack cares about it. A Distribution
844
+ in `eu-west-2` with a certificate alongside it looks fine until CloudFront refuses it.
845
+
846
+ ```typescript sim-cloudfront-viewer-certificate
847
+ /**
848
+ * Catching an ACM certificate CloudFront will not accept.
849
+ */
850
+
851
+ import { RequestCertificateCommand } from "@aws-sdk/client-acm";
852
+ import { CreateDistributionCommand } from "@aws-sdk/client-cloudfront";
853
+
854
+ import { SimAws } from "@kensio/yulin";
855
+
856
+ const simAws = new SimAws();
857
+
858
+ // A certificate alongside the rest of the stack, rather than in us-east-1.
859
+ const requestOutput = await simAws
860
+ .region("eu-west-2")
861
+ .acm()
862
+ .requestCertificate(
863
+ new RequestCertificateCommand({ DomainName: "example.test" }),
864
+ );
865
+
866
+ await simAws.backgroundTasksComplete();
867
+
868
+ try {
869
+ await simAws.cloudFront().createDistribution(
870
+ new CreateDistributionCommand({
871
+ DistributionConfig: {
872
+ CallerReference: "site-distribution",
873
+ Comment: "Site distribution",
874
+ Enabled: true,
875
+ Aliases: { Quantity: 1, Items: ["example.test"] },
876
+ Origins: { Quantity: 0, Items: [] },
877
+ DefaultCacheBehavior: {
878
+ TargetOriginId: "origin",
879
+ ViewerProtocolPolicy: "redirect-to-https",
880
+ },
881
+ ViewerCertificate: {
882
+ ACMCertificateArn: requestOutput.CertificateArn,
883
+ SSLSupportMethod: "sni-only",
884
+ },
885
+ },
886
+ }),
887
+ );
888
+ } catch (error) {
889
+ // InvalidViewerCertificate: ... is in eu-west-2, but CloudFront only accepts
890
+ // ACM Certificates in us-east-1
891
+ console.log((error as Error).message);
892
+ }
893
+ ```
894
+
895
+ The CloudFront API and CloudFormation capitalise this field differently, and sim CloudFront accepts
896
+ both. SDK calls use `ACMCertificateArn` and `SSLSupportMethod`, as above.
897
+ `AWS::CloudFront::Distribution` uses `AcmCertificateArn` and `SslSupportMethod`. A template or CDK
898
+ app works without changes.
899
+
900
+ A Distribution using `CloudFrontDefaultCertificate` needs no ACM certificate, and it goes
901
+ unchecked. A standalone `new SimCloudFront()` has no sim ACM to check against, and skips the check
902
+ as well.
903
+
904
+ ## Disabling and deleting a Distribution
905
+
906
+ `DeleteDistributionCommand` removes a Distribution. CloudFront will only delete one that has stopped
907
+ serving. The sequence is `UpdateDistributionCommand` with `Enabled: false` first, then the deletion. Deleting an enabled Distribution answers `DistributionNotDisabled`, as it does in AWS.
908
+
909
+ `UpdateDistributionCommand` takes a whole `DistributionConfig`, and applies the update as a
910
+ replacement. Anything left out of the new config is dropped, including alternate domain names and
911
+ the default root object. Read the Distribution first, change the field you want, and send the config
912
+ back.
913
+
914
+ Once the Distribution is deleted, a request to its CloudFront domain or any of its alternate domain
915
+ names stops resolving to it, and those alternate domain names are free for another Distribution.
916
+
917
+ ```typescript sim-cloudfront-delete-distribution
918
+ /**
919
+ * Disabling a simulated CloudFront Distribution and then deleting it.
920
+ */
921
+
922
+ import {
923
+ CreateDistributionCommand,
924
+ DeleteDistributionCommand,
925
+ type DistributionConfig,
926
+ GetDistributionCommand,
927
+ UpdateDistributionCommand,
928
+ } from "@aws-sdk/client-cloudfront";
929
+ import { CreateBucketCommand } from "@aws-sdk/client-s3";
930
+
931
+ import { SimAws } from "@kensio/yulin";
932
+
933
+ const simAws = new SimAws();
934
+ const simCloudFront = simAws.cloudFront();
935
+
936
+ await simAws
937
+ .s3()
938
+ .createBucket(new CreateBucketCommand({ Bucket: "site-bucket" }));
939
+
940
+ const distributionConfig: DistributionConfig = {
941
+ CallerReference: "site-distribution",
942
+ Comment: "Site distribution",
943
+ Enabled: true,
944
+ Origins: {
945
+ Quantity: 1,
946
+ Items: [
947
+ {
948
+ Id: "site-origin",
949
+ DomainName: "site-bucket.s3.amazonaws.com",
950
+ S3OriginConfig: { OriginAccessIdentity: "" },
951
+ },
952
+ ],
953
+ },
954
+ DefaultCacheBehavior: {
955
+ TargetOriginId: "site-origin",
956
+ ViewerProtocolPolicy: "allow-all",
957
+ },
958
+ };
959
+
960
+ const created = await simCloudFront.createDistribution(
961
+ new CreateDistributionCommand({ DistributionConfig: distributionConfig }),
962
+ );
963
+ await simAws.backgroundTasksComplete();
964
+
965
+ const distributionId = created.Distribution?.Id;
966
+
967
+ try {
968
+ await simCloudFront.deleteDistribution(
969
+ new DeleteDistributionCommand({ Id: distributionId }),
970
+ );
971
+ } catch (error) {
972
+ // DistributionNotDisabled: Sim CloudFront Distribution ... is enabled, so it
973
+ // cannot be deleted. Disable it with UpdateDistribution first.
974
+ console.log((error as Error).message);
975
+ }
976
+
977
+ // Disable the Distribution, then delete it.
978
+ await simCloudFront.updateDistribution(
979
+ new UpdateDistributionCommand({
980
+ Id: distributionId,
981
+ DistributionConfig: { ...distributionConfig, Enabled: false },
982
+ }),
983
+ );
984
+ await simAws.backgroundTasksComplete();
985
+
986
+ await simCloudFront.deleteDistribution(
987
+ new DeleteDistributionCommand({ Id: distributionId }),
988
+ );
989
+
990
+ try {
991
+ await simCloudFront.getDistribution(
992
+ new GetDistributionCommand({ Id: distributionId }),
993
+ );
994
+ } catch (error) {
995
+ // NoSuchDistribution: No sim CloudFront Distribution with ID ...
996
+ console.log((error as Error).message);
997
+ }
998
+ ```
999
+
1000
+ `DeleteFunctionCommand` removes a CloudFront Function by name, and answers `NoSuchFunctionExists`
1001
+ when the name matches nothing. A cache Behavior still pointing at a deleted Function runs no
1002
+ Function code.
1003
+
1004
+ ## Simulated CloudFront Functions
1005
+
1006
+ The sim CloudFront supports `viewer-request` and `viewer-response` CloudFront Functions.
1007
+
1008
+ A `viewer-response` Function runs for an Origin status below 400. CloudFront skips the
1009
+ viewer-response event once the Origin has answered 400 or higher (see
1010
+ [Limitations](#limitations)), and so does this simulation.
1011
+
1012
+ Use `makeCffFunctionCodeInput` to pass a JavaScript handler function to `CreateFunctionCommand`.
1013
+
1014
+ The `host` header a function sees is the hostname the request was made to CloudFront with, being the
1015
+ Distribution domain name or one of its alternate domain names. Requests served on localhost arrive
1016
+ with a Yulin-local host such as `distro123.cloudfront.net.sim-aws.localhost:52341`, and the local
1017
+ suffix and port are dropped before the function runs. A function building a URL from
1018
+ `event.request.headers.host.value` behaves as it would on AWS. As on AWS, `host` is read-only, and a
1019
+ host a function writes is discarded before the Origin sees it.
1020
+
1021
+ A header arriving more than once reaches the Function as one entry holding every value it arrived
1022
+ with. `value` carries the first, and `multiValue` carries all of them, the same shape a repeated
1023
+ query string parameter has. A response setting three cookies gives a viewer-response Function this:
1024
+
1025
+ ```typescript
1026
+ event.response.headers["set-cookie"];
1027
+ // {
1028
+ // value: "session=abc123; Path=/",
1029
+ // multiValue: [
1030
+ // { value: "session=abc123; Path=/" },
1031
+ // { value: "state=; Max-Age=0" },
1032
+ // { value: "signed-in=1; Path=/" },
1033
+ // ],
1034
+ // }
1035
+ ```
1036
+
1037
+ A Function returning that response untouched leaves all three cookies on their way to the viewer. A
1038
+ Function writing `multiValue` sends one header per value in it, and CloudFront ignores `value` while
1039
+ both are there. Writing `value` on its own sends a single header.
1040
+
1041
+ A Function reads the query string as the viewer spelled it. `?q=%E5%AE%B6` arrives as
1042
+ `event.request.querystring.q.value === "%E5%AE%B6"`, `?q=a+b` keeps its plus, and a percent-encoded
1043
+ parameter name stays encoded. Whatever a Function leaves in `querystring` goes on to the Origin as
1044
+ it stands. A Function returning the request untouched forwards the query byte for byte, and one
1045
+ writing a value of its own encodes it (the same job it has on AWS).
1046
+
1047
+ ```typescript sim-cloudfront-function
1048
+ /**
1049
+ * Simulated CloudFront Functions.
1050
+ */
1051
+
1052
+ import {
1053
+ CreateDistributionCommand,
1054
+ CreateFunctionCommand,
1055
+ } from "@aws-sdk/client-cloudfront";
1056
+ import {
1057
+ CreateBucketCommand,
1058
+ PutBucketPolicyCommand,
1059
+ PutPublicAccessBlockCommand,
1060
+ } from "@aws-sdk/client-s3";
1061
+
1062
+ import { SimAws } from "@kensio/yulin";
1063
+ import {
1064
+ makeCffFunctionCodeInput,
1065
+ type CloudFrontFunction,
1066
+ } from "@kensio/yulin/cloudfront";
1067
+ import { serveSimAws } from "@kensio/yulin/serve";
1068
+
1069
+ const simAws = new SimAws();
1070
+ const srv = await serveSimAws({ simAws });
1071
+
1072
+ try {
1073
+ const simS3 = simAws.s3();
1074
+ const simCloudFront = simAws.cloudFront();
1075
+
1076
+ await simS3.createBucket(
1077
+ new CreateBucketCommand({
1078
+ Bucket: "foo-bucket",
1079
+ }),
1080
+ );
1081
+
1082
+ // A CloudFront S3 Origin with no origin access control reads the Bucket
1083
+ // anonymously, so what it serves has to be publicly readable.
1084
+ await simS3.putPublicAccessBlock(
1085
+ new PutPublicAccessBlockCommand({
1086
+ Bucket: "foo-bucket",
1087
+ PublicAccessBlockConfiguration: {
1088
+ BlockPublicAcls: true,
1089
+ IgnorePublicAcls: true,
1090
+ },
1091
+ }),
1092
+ );
1093
+ await simS3.putBucketPolicy(
1094
+ new PutBucketPolicyCommand({
1095
+ Bucket: "foo-bucket",
1096
+ Policy: JSON.stringify({
1097
+ Version: "2012-10-17",
1098
+ Statement: {
1099
+ Effect: "Allow",
1100
+ Principal: "*",
1101
+ Action: "s3:GetObject",
1102
+ Resource: "arn:aws:s3:::foo-bucket/*",
1103
+ },
1104
+ }),
1105
+ }),
1106
+ );
1107
+
1108
+ function viewerRequestFunction(
1109
+ event: CloudFrontFunction.ViewerRequestEvent,
1110
+ ): CloudFrontFunction.Request | CloudFrontFunction.Response {
1111
+ if (event.request.uri === "/old-page.html") {
1112
+ return {
1113
+ statusCode: 302,
1114
+ statusDescription: "Found",
1115
+ headers: {
1116
+ location: {
1117
+ value: "https://example.test/new-page.html",
1118
+ },
1119
+ },
1120
+ };
1121
+ }
1122
+
1123
+ return event.request;
1124
+ }
1125
+
1126
+ const functionCreation = await simCloudFront.createFunction(
1127
+ new CreateFunctionCommand({
1128
+ Name: "redirect-old-page",
1129
+ FunctionConfig: {
1130
+ Comment: "Redirect old page",
1131
+ Runtime: "cloudfront-js-2.0",
1132
+ },
1133
+ FunctionCode: makeCffFunctionCodeInput(viewerRequestFunction),
1134
+ }),
1135
+ );
1136
+
1137
+ const distributionCreation = await simCloudFront.createDistribution(
1138
+ new CreateDistributionCommand({
1139
+ DistributionConfig: {
1140
+ CallerReference: "function-cdn",
1141
+ Comment: "Function CDN",
1142
+ Enabled: true,
1143
+ Origins: {
1144
+ Quantity: 1,
1145
+ Items: [
1146
+ {
1147
+ Id: "assets-origin",
1148
+ DomainName: "foo-bucket.s3.amazonaws.com",
1149
+ S3OriginConfig: {
1150
+ OriginAccessIdentity: "",
1151
+ },
1152
+ },
1153
+ ],
1154
+ },
1155
+ DefaultCacheBehavior: {
1156
+ TargetOriginId: "assets-origin",
1157
+ ViewerProtocolPolicy: "allow-all",
1158
+ FunctionAssociations: {
1159
+ Quantity: 1,
1160
+ Items: [
1161
+ {
1162
+ EventType: "viewer-request",
1163
+ FunctionARN: functionCreation.FunctionMetadata.FunctionARN,
1164
+ },
1165
+ ],
1166
+ },
1167
+ },
1168
+ },
1169
+ }),
1170
+ );
1171
+
1172
+ const distroHostname = distributionCreation.Distribution!.DomainName!;
1173
+
1174
+ const url = srv.localUrl(`http://${distroHostname}/old-page.html`);
1175
+ const response = await fetch(url, { redirect: "manual" });
1176
+
1177
+ console.log(response.status);
1178
+ console.log(response.headers.get("location"));
1179
+ } finally {
1180
+ await srv.close();
1181
+ }
1182
+ ```
1183
+
1184
+ If your CloudFront Function code lives in a module that exports the handler, use
1185
+ `cloudFrontFunctionSourceFromModule` in your CDK Stack to load it as inline CloudFront Function
1186
+ code. This lets the same function file use an export like `export function handler(...)` while
1187
+ still being accepted by CloudFront Function inline code.
1188
+
1189
+ ```typescript sim-cloudfront-function-module-export
1190
+ /**
1191
+ * cloudFrontFunctionSourceFromModule util function
1192
+ */
1193
+
1194
+ import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
1195
+ import { Stack } from "aws-cdk-lib";
1196
+ import type { Construct } from "constructs";
1197
+
1198
+ import { cloudFrontFunctionSourceFromModule } from "@kensio/yulin/cloudfront";
1199
+
1200
+ /**
1201
+ * Example CDK stack using cloudFrontFunctionSourceFromModule to extract source
1202
+ * code for a CloudFront Function handler from a module that uses `export`.
1203
+ */
1204
+ export class WebsiteStack extends Stack {
1205
+ constructor(scope: Construct, id: string) {
1206
+ super(scope, id);
1207
+
1208
+ new cloudfront.Function(this, "RewriteFunction", {
1209
+ code: cloudfront.FunctionCode.fromInline(
1210
+ cloudFrontFunctionSourceFromModule("src/cff/rewrite.cff.js"),
1211
+ ),
1212
+ runtime: cloudfront.FunctionRuntime.JS_2_0,
1213
+ });
1214
+ }
1215
+ }
1216
+ ```
1217
+
1218
+ The referenced CloudFront Function module can then keep an exported handler:
1219
+
1220
+ ```javascript
1221
+ /**
1222
+ * @typedef {import("@kensio/yulin/cloudfront").CloudFrontFunction.Event} CloudFrontEvent
1223
+ * @typedef {import("@kensio/yulin/cloudfront").CloudFrontFunction.Request} CloudFrontRequest
1224
+ * @typedef {import("@kensio/yulin/cloudfront").CloudFrontFunction.Response} CloudFrontResponse
1225
+ */
1226
+
1227
+ /**
1228
+ * Handles a CloudFront Functions viewer request event.
1229
+ * @param {CloudFrontEvent} event - The CloudFront Functions event object.
1230
+ * @returns {CloudFrontRequest|CloudFrontResponse} A CloudFront request object or response object.
1231
+ */
1232
+ export function handler(event) {
1233
+ var request = event.request;
1234
+ var uri = request.uri;
1235
+
1236
+ if (uri.endsWith("/")) {
1237
+ request.uri += "index.html";
1238
+ } else if (!uri.includes(".") && !uri.endsWith("/")) {
1239
+ request.uri += "/index.html";
1240
+ }
1241
+
1242
+ return request;
1243
+ }
1244
+ ```
1245
+
1246
+ CloudFront Functions run JS2, ECMAScript 5.1 plus a named subset of ES 6 to 12. It refuses
1247
+ constructs ordinary JavaScript allows. Yulin publishes ESLint and Oxlint configs that report those
1248
+ refusals in the editor, ahead of publication. See
1249
+ [Linting CloudFront Functions JS2](https://yulinsim.dev/lint/ "CloudFront Functions JS2 lint config usage docs").
1250
+
1251
+ CloudFront also caps Function code at 10 KB, counted on the source as uploaded, comments and all.
1252
+ Simulated `CreateFunction` refuses anything larger with `FunctionSizeLimitExceeded`, as the real
1253
+ service does. A test that deploys the Stack reports the overrun where the rest of the suite runs,
1254
+ ahead of `cdk deploy`. A handler passed as a function reference carries no source to count, and the
1255
+ limit leaves it alone.
1256
+
1257
+ ### Calling a Function handler without a Distribution
1258
+
1259
+ A test of the handler on its own, with no Distribution in front of it, still has to pass it a whole
1260
+ event. `cloudFrontViewerRequestEventFactory` and `cloudFrontViewerResponseEventFactory` make the
1261
+ two, so such a test says what the request or the response was and leaves the rest alone:
1262
+
1263
+ ```typescript sim-cloudfront-function-event-factory
1264
+ /**
1265
+ * Making a CloudFront Functions event to call a handler with.
1266
+ */
1267
+
1268
+ import { VariantFactory } from "@kensio/part-factory";
1269
+
1270
+ import {
1271
+ cloudFrontViewerResponseEventFactory,
1272
+ type CloudFrontFunction,
1273
+ } from "@kensio/yulin/cloudfront";
1274
+
1275
+ function securityHeadersHandler(
1276
+ event: CloudFrontFunction.ViewerResponseEvent,
1277
+ ): CloudFrontFunction.Response {
1278
+ const response = event.response;
1279
+ const contentType = response.headers["content-type"]?.value ?? "";
1280
+
1281
+ if (contentType.startsWith("text/html")) {
1282
+ response.headers["x-frame-options"] = { value: "DENY" };
1283
+ }
1284
+
1285
+ return response;
1286
+ }
1287
+
1288
+ // A response carrying a page. Those are the ones the policy is about.
1289
+ const documentResponseFactory = new VariantFactory(
1290
+ cloudFrontViewerResponseEventFactory,
1291
+ {
1292
+ response: {
1293
+ headers: { "content-type": { value: "text/html; charset=utf-8" } },
1294
+ },
1295
+ },
1296
+ );
1297
+
1298
+ const page = securityHeadersHandler(documentResponseFactory.make());
1299
+
1300
+ // DENY
1301
+ console.log(page.headers["x-frame-options"]?.value);
1302
+
1303
+ // One response, for a test about a single asset. Everything else about it, down
1304
+ // to the request that asked for it, is filled in as a served response's is.
1305
+ const asset = securityHeadersHandler(
1306
+ cloudFrontViewerResponseEventFactory.make({
1307
+ response: { headers: { "content-type": { value: "text/css" } } },
1308
+ }),
1309
+ );
1310
+
1311
+ // undefined
1312
+ console.log(asset.headers["x-frame-options"]?.value);
1313
+ ```
1314
+
1315
+ The defaults describe a request for `/cloudfront/` reaching the Distribution, with a `host` of
1316
+ `yulin.test`, a session cookie and a viewer address. A viewer-response event carries the request
1317
+ that asked for it as well as the response, and the response's own defaults are a status code and no
1318
+ headers.
1319
+
1320
+ The [event factories page](https://yulinsim.dev/factories/ "Test factories for AWS event shapes usage docs")
1321
+ covers what the factories have in common.
1322
+
1323
+ ## Simulated Lambda@Edge
1324
+
1325
+ A cache Behavior can run a Lambda function at any of CloudFront's four events through
1326
+ `LambdaFunctionAssociations`. Those are `viewer-request` and `viewer-response` at the edge, and
1327
+ `origin-request` and `origin-response` either side of the Origin fetch. Where a CloudFront Function
1328
+ is a small piece of JavaScript running in CloudFront's own runtime, a Lambda@Edge function is an
1329
+ ordinary simulated Lambda function, with an execution role, an environment and whatever SDK calls
1330
+ its handler makes.
1331
+
1332
+ Three things about Lambda@Edge catch people out on AWS, and simulated CloudFront refuses all three
1333
+ the way AWS refuses them, when the Distribution is written rather than when a request arrives.
1334
+
1335
+ - **The function lives in `us-east-1`**, wherever the rest of the stack lives.
1336
+ - **The association names a published version**, such as `:1`. An unqualified ARN, `$LATEST` and an
1337
+ alias are each refused.
1338
+ - **The execution role trusts `edgelambda.amazonaws.com`** as well as `lambda.amazonaws.com`. A role
1339
+ set up for an ordinary function is the usual reason a first Lambda@Edge deploy fails.
1340
+
1341
+ ```typescript sim-cloudfront-lambda-edge
1342
+ /**
1343
+ * A Lambda@Edge function rewriting a request at the viewer.
1344
+ */
1345
+
1346
+ import { CreateDistributionCommand } from "@aws-sdk/client-cloudfront";
1347
+ import { CreateRoleCommand } from "@aws-sdk/client-iam";
1348
+ import {
1349
+ CreateFunctionCommand,
1350
+ PublishVersionCommand,
1351
+ } from "@aws-sdk/client-lambda";
1352
+
1353
+ import { SimAws } from "@kensio/yulin";
1354
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
1355
+ import type { LambdaAtEdge } from "@kensio/yulin/cloudfront";
1356
+
1357
+ const simAws = new SimAws();
1358
+
1359
+ // A Lambda@Edge execution role trusts both service principals.
1360
+ const role = await simAws.iam().createRole(
1361
+ new CreateRoleCommand({
1362
+ RoleName: "EdgeRewriteRole",
1363
+ AssumeRolePolicyDocument: JSON.stringify({
1364
+ Version: "2012-10-17",
1365
+ Statement: {
1366
+ Effect: "Allow",
1367
+ Principal: {
1368
+ Service: ["lambda.amazonaws.com", "edgelambda.amazonaws.com"],
1369
+ },
1370
+ Action: "sts:AssumeRole",
1371
+ },
1372
+ }),
1373
+ }),
1374
+ );
1375
+
1376
+ // The function has to be in us-east-1, and the Behavior names a version.
1377
+ const edgeLambda = simAws.region("us-east-1").lambda();
1378
+
1379
+ await edgeLambda.createFunction(
1380
+ new CreateFunctionCommand({
1381
+ FunctionName: "rewrite-uri",
1382
+ Role: role.Role.Arn,
1383
+ Code: {
1384
+ ZipFile: makeLambdaZipFileInput((event: LambdaAtEdge.RequestEvent) => {
1385
+ const { request } = event.Records[0].cf;
1386
+
1387
+ // A header is a list keyed by its lowercase name, and a status is a
1388
+ // string. Both differ from the CloudFront Functions shapes.
1389
+ if (request.headers["x-preview"]?.[0]?.value === "1") {
1390
+ return {
1391
+ status: "302",
1392
+ headers: {
1393
+ location: [{ key: "Location", value: "/preview.html" }],
1394
+ },
1395
+ };
1396
+ }
1397
+
1398
+ request.uri = "/index.html";
1399
+
1400
+ return request;
1401
+ }),
1402
+ },
1403
+ }),
1404
+ );
1405
+
1406
+ const version = await edgeLambda.publishVersion(
1407
+ new PublishVersionCommand({ FunctionName: "rewrite-uri" }),
1408
+ );
1409
+
1410
+ await simAws.cloudFront().createDistribution(
1411
+ new CreateDistributionCommand({
1412
+ DistributionConfig: {
1413
+ CallerReference: "edge-rewrite",
1414
+ Comment: "Rewriting at the viewer",
1415
+ Enabled: true,
1416
+ Origins: {
1417
+ Quantity: 1,
1418
+ Items: [
1419
+ {
1420
+ Id: "site-origin",
1421
+ DomainName: "edge-site.s3.amazonaws.com",
1422
+ S3OriginConfig: { OriginAccessIdentity: "" },
1423
+ },
1424
+ ],
1425
+ },
1426
+ DefaultCacheBehavior: {
1427
+ TargetOriginId: "site-origin",
1428
+ ViewerProtocolPolicy: "allow-all",
1429
+ LambdaFunctionAssociations: {
1430
+ Quantity: 1,
1431
+ Items: [
1432
+ {
1433
+ EventType: "viewer-request",
1434
+ LambdaFunctionARN: version.FunctionArn,
1435
+ IncludeBody: false,
1436
+ },
1437
+ ],
1438
+ },
1439
+ },
1440
+ },
1441
+ }),
1442
+ );
1443
+ ```
1444
+
1445
+ A `viewer-request` handler returning the request carries on to the Origin with whatever it changed.
1446
+ Returning a response answers the viewer there and then, and the Origin is never read. A
1447
+ `viewer-response` handler returns the response the viewer gets.
1448
+
1449
+ Set `IncludeBody` to give a `viewer-request` or `origin-request` handler the request body, which
1450
+ arrives base64 encoded under `request.body.data`. A handler setting `request.body.action` to
1451
+ `replace` sends its own body to the Origin. The field belongs to the two request events, and an
1452
+ association setting it on `viewer-response` or `origin-response` is refused, as CloudFront refuses
1453
+ one.
1454
+
1455
+ A handler that throws answers the viewer with a 502, as CloudFront answers a failed edge function.
1456
+ The error reaches the function's own output and nothing else.
1457
+
1458
+ ### The origin events
1459
+
1460
+ An `origin-request` function runs after the Behavior has resolved the Origin and before the fetch.
1461
+ Its event carries `request.origin`, holding the Origin the fetch is about to read, under `custom` or
1462
+ `s3` for the kind it is. A handler rewriting `origin.custom.domainName` sends the fetch to another
1463
+ Origin, and one rewriting `path` reads under another prefix. A header added to `customHeaders`
1464
+ reaches the Origin and the viewer never sees it. A handler returning a response answers the viewer
1465
+ with the Origin unread.
1466
+
1467
+ An `origin-response` function runs after the fetch and before the custom error page replaces an
1468
+ error status. It runs on whatever the Origin answered, including a 400 and above. That is where the
1469
+ origin events differ from the viewer events, and CloudFront documents it. Returning a response
1470
+ replaces what the viewer gets.
1471
+
1472
+ At both origin events the `host` header holds the Origin's own domain name. A viewer event shows the
1473
+ domain the viewer used.
1474
+
1475
+ ```typescript sim-cloudfront-lambda-edge-origin
1476
+ /**
1477
+ * A Lambda@Edge function choosing the Origin, and another one stamping what
1478
+ * that Origin answered.
1479
+ */
1480
+
1481
+ import { CreateDistributionCommand } from "@aws-sdk/client-cloudfront";
1482
+ import { CreateRoleCommand } from "@aws-sdk/client-iam";
1483
+ import {
1484
+ CreateFunctionCommand,
1485
+ PublishVersionCommand,
1486
+ } from "@aws-sdk/client-lambda";
1487
+
1488
+ import { SimAws } from "@kensio/yulin";
1489
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
1490
+ import type { LambdaAtEdge } from "@kensio/yulin/cloudfront";
1491
+
1492
+ const simAws = new SimAws();
1493
+
1494
+ // A Lambda@Edge execution role trusts both service principals, at the origin
1495
+ // events as at the viewer events.
1496
+ const role = await simAws.iam().createRole(
1497
+ new CreateRoleCommand({
1498
+ RoleName: "EdgeOriginRole",
1499
+ AssumeRolePolicyDocument: JSON.stringify({
1500
+ Version: "2012-10-17",
1501
+ Statement: {
1502
+ Effect: "Allow",
1503
+ Principal: {
1504
+ Service: ["lambda.amazonaws.com", "edgelambda.amazonaws.com"],
1505
+ },
1506
+ Action: "sts:AssumeRole",
1507
+ },
1508
+ }),
1509
+ }),
1510
+ );
1511
+
1512
+ const edgeLambda = simAws.region("us-east-1").lambda();
1513
+
1514
+ await edgeLambda.createFunction(
1515
+ new CreateFunctionCommand({
1516
+ FunctionName: "route-origin",
1517
+ Role: role.Role.Arn,
1518
+ Code: {
1519
+ ZipFile: makeLambdaZipFileInput(
1520
+ (event: LambdaAtEdge.OriginRequestEvent) => {
1521
+ const { request } = event.Records[0].cf;
1522
+ const { custom } = request.origin;
1523
+
1524
+ if (custom === undefined) {
1525
+ return request;
1526
+ }
1527
+
1528
+ // Everything under /api is served by the second Origin.
1529
+ if (request.uri.startsWith("/api/")) {
1530
+ custom.domainName = "orders.example.test";
1531
+ }
1532
+
1533
+ // A header the viewer never sent and never sees.
1534
+ custom.customHeaders["x-from-cloudfront"] = [
1535
+ { key: "X-From-CloudFront", value: "yes" },
1536
+ ];
1537
+
1538
+ return request;
1539
+ },
1540
+ ),
1541
+ },
1542
+ }),
1543
+ );
1544
+
1545
+ const routeVersion = await edgeLambda.publishVersion(
1546
+ new PublishVersionCommand({ FunctionName: "route-origin" }),
1547
+ );
1548
+
1549
+ await edgeLambda.createFunction(
1550
+ new CreateFunctionCommand({
1551
+ FunctionName: "stamp-origin-response",
1552
+ Role: role.Role.Arn,
1553
+ Code: {
1554
+ ZipFile: makeLambdaZipFileInput(
1555
+ (event: LambdaAtEdge.OriginResponseEvent): LambdaAtEdge.Response => {
1556
+ const { response } = event.Records[0].cf;
1557
+
1558
+ // This runs for an Origin error too, so the status is worth keeping.
1559
+ return {
1560
+ ...response,
1561
+ headers: {
1562
+ ...response.headers,
1563
+ "x-origin-status": [
1564
+ { key: "X-Origin-Status", value: response.status },
1565
+ ],
1566
+ },
1567
+ };
1568
+ },
1569
+ ),
1570
+ },
1571
+ }),
1572
+ );
1573
+
1574
+ const stampVersion = await edgeLambda.publishVersion(
1575
+ new PublishVersionCommand({ FunctionName: "stamp-origin-response" }),
1576
+ );
1577
+
1578
+ const customOriginConfig = {
1579
+ HTTPPort: 80,
1580
+ HTTPSPort: 443,
1581
+ OriginProtocolPolicy: "https-only",
1582
+ } as const;
1583
+
1584
+ await simAws.cloudFront().createDistribution(
1585
+ new CreateDistributionCommand({
1586
+ DistributionConfig: {
1587
+ CallerReference: "edge-origin-routing",
1588
+ Comment: "Choosing the Origin at the edge",
1589
+ Enabled: true,
1590
+ Origins: {
1591
+ Quantity: 2,
1592
+ Items: [
1593
+ {
1594
+ Id: "site-origin",
1595
+ DomainName: "site.example.test",
1596
+ CustomOriginConfig: customOriginConfig,
1597
+ },
1598
+ {
1599
+ Id: "orders-origin",
1600
+ DomainName: "orders.example.test",
1601
+ CustomOriginConfig: customOriginConfig,
1602
+ },
1603
+ ],
1604
+ },
1605
+ DefaultCacheBehavior: {
1606
+ TargetOriginId: "site-origin",
1607
+ ViewerProtocolPolicy: "allow-all",
1608
+ LambdaFunctionAssociations: {
1609
+ Quantity: 2,
1610
+ Items: [
1611
+ {
1612
+ EventType: "origin-request",
1613
+ LambdaFunctionARN: routeVersion.FunctionArn,
1614
+ },
1615
+ {
1616
+ EventType: "origin-response",
1617
+ LambdaFunctionARN: stampVersion.FunctionArn,
1618
+ },
1619
+ ],
1620
+ },
1621
+ },
1622
+ },
1623
+ }),
1624
+ );
1625
+ ```
1626
+
1627
+ Two Origin rewrites are refused, and the viewer gets the 502 a failed edge function gets, carrying
1628
+ the reason (see [Limitations](#limitations)). One switches an Origin between `custom` and `s3`. The
1629
+ other moves an S3 Origin to another Bucket.
1630
+
1631
+ ### Which edge function runs where
1632
+
1633
+ CloudFront takes one edge function per event type, and it does not combine CloudFront Functions with
1634
+ Lambda@Edge at the viewer events. A Behavior with a viewer-request CloudFront Function and a
1635
+ viewer-response Lambda@Edge function is refused, and so is a Behavior naming both at one event type.
1636
+ Simulated CloudFront refuses the same combinations. The rule stops at the viewer. A viewer-request
1637
+ CloudFront Function runs alongside a Lambda@Edge function on either origin event.
1638
+
1639
+ Neither kind runs at the viewer response once the Origin has answered 400 or higher. CloudFront
1640
+ skips that event for an Origin error, and the status the Origin returned is what decides it (see
1641
+ [Limitations](#limitations)).
1642
+
1643
+ Both kinds of function see the `host` header as the hostname the viewer reached CloudFront with,
1644
+ rather than the Yulin-local host a request served on localhost arrives with. As on AWS, `host` is
1645
+ read-only at the viewer request, and a host a handler writes is discarded before the Origin sees it.
1646
+
1647
+ ### From CloudFormation
1648
+
1649
+ `AWS::CloudFront::Distribution` takes `LambdaFunctionAssociations` on `DefaultCacheBehavior` and on
1650
+ any entry of `CacheBehaviors`. CloudFormation writes the list as a plain array where the SDK writes
1651
+ the `Quantity` and `Items` pair. `Ref` on an `AWS::Lambda::Version` answers the qualified function
1652
+ ARN. That is the value an association names, and the two fit together directly:
1653
+
1654
+ ```yaml
1655
+ EdgeVersion:
1656
+ Type: AWS::Lambda::Version
1657
+ Properties:
1658
+ FunctionName: !Ref RewriteFunction
1659
+
1660
+ SiteDistribution:
1661
+ Type: AWS::CloudFront::Distribution
1662
+ Properties:
1663
+ DistributionConfig:
1664
+ DefaultCacheBehavior:
1665
+ TargetOriginId: SiteOrigin
1666
+ ViewerProtocolPolicy: allow-all
1667
+ LambdaFunctionAssociations:
1668
+ - EventType: viewer-request
1669
+ LambdaFunctionARN: !Ref EdgeVersion
1670
+ ```
1671
+
1672
+ The function still has to live in us-east-1. A stack holding one is a us-east-1 stack.
1673
+
1674
+ An association naming a function version this simulation does not hold is left out of the deployed
1675
+ Distribution and recorded on `stack.ignoredProperties`, under the event type it was on. A template
1676
+ pointing at a function in a real account is the usual reason. The rest of the Behavior deploys, and
1677
+ the event the skipped association was on is left empty. A test that cares reads the record.
1678
+
1679
+ Everything real CloudFront refuses still fails the deployment. A function outside us-east-1, an ARN
1680
+ without a version qualifier, an execution role missing the `edgelambda.amazonaws.com` trust, an
1681
+ `EventType` that is none of CloudFront's four, two functions on one event type and a viewer event
1682
+ running both kinds of edge function each fail a real deploy of the same template.
1683
+
1684
+ CDK reaches a Behavior through `edgeLambdas`, given a `lambda.Version` from the same stack:
1685
+
1686
+ ```typescript sim-cloudfront-lambda-edge-cdk
1687
+ /**
1688
+ * A CDK Distribution running a Lambda@Edge function at the viewer request.
1689
+ */
1690
+
1691
+ import { Stack } from "aws-cdk-lib";
1692
+ import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
1693
+ import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
1694
+ import * as iam from "aws-cdk-lib/aws-iam";
1695
+ import * as lambda from "aws-cdk-lib/aws-lambda";
1696
+ import * as s3 from "aws-cdk-lib/aws-s3";
1697
+ import type { Construct } from "constructs";
1698
+
1699
+ /**
1700
+ * Example CDK stack whose Distribution rewrites every request at the edge.
1701
+ *
1702
+ * The stack is in us-east-1, the one Region CloudFront runs a Lambda@Edge
1703
+ * function from.
1704
+ */
1705
+ export class SiteStack extends Stack {
1706
+ constructor(scope: Construct, id: string) {
1707
+ super(scope, id, { env: { region: "us-east-1" } });
1708
+
1709
+ const siteBucket = new s3.Bucket(this, "SiteBucket");
1710
+
1711
+ // A Lambda@Edge execution role trusts both service principals.
1712
+ const edgeRole = new iam.Role(this, "EdgeRole", {
1713
+ assumedBy: new iam.CompositePrincipal(
1714
+ new iam.ServicePrincipal("lambda.amazonaws.com"),
1715
+ new iam.ServicePrincipal("edgelambda.amazonaws.com"),
1716
+ ),
1717
+ });
1718
+
1719
+ const rewriteFunction = new lambda.Function(this, "RewriteFunction", {
1720
+ runtime: lambda.Runtime.NODEJS_22_X,
1721
+ handler: "index.handler",
1722
+ role: edgeRole,
1723
+ code: lambda.Code.fromInline(`
1724
+ exports.handler = async (event) => {
1725
+ const { request } = event.Records[0].cf;
1726
+ request.uri = "/index.html";
1727
+ return request;
1728
+ };
1729
+ `),
1730
+ });
1731
+
1732
+ new cloudfront.Distribution(this, "SiteDistribution", {
1733
+ defaultBehavior: {
1734
+ origin: origins.S3BucketOrigin.withOriginAccessControl(siteBucket),
1735
+ edgeLambdas: [
1736
+ {
1737
+ // edgeLambdas takes a published version, and currentVersion
1738
+ // is one.
1739
+ functionVersion: rewriteFunction.currentVersion,
1740
+ eventType: cloudfront.LambdaEdgeEventType.VIEWER_REQUEST,
1741
+ },
1742
+ ],
1743
+ },
1744
+ });
1745
+ }
1746
+ }
1747
+ ```
1748
+
1749
+ `cloudfront.experimental.EdgeFunction` deploys from a us-east-1 stack, where the construct creates
1750
+ the function alongside everything else.
1751
+
1752
+ From a stack in any other Region the construct writes the function, its published version and an SSM
1753
+ parameter holding the version ARN into a support stack in us-east-1. The stack using the function
1754
+ reads that parameter back through a `Custom::CrossRegionStringParameterReader` resource, and the
1755
+ Behavior's `LambdaFunctionARN` is an `Fn::GetAtt` on it. Simulated CloudFormation makes that read
1756
+ itself, against the Region the resource names, and the association ends up holding the ARN the
1757
+ support stack published. Both stacks have to deploy, which is what deploying the whole cloud
1758
+ assembly does:
1759
+
1760
+ ```typescript
1761
+ await simAws.cloudFormation().deployCdkOut("cdk.out");
1762
+ ```
1763
+
1764
+ Deploy the using stack's template on its own and no parameter has been written. The read finds
1765
+ nothing and says so on `stack.ignoredProperties`, the Behavior deploys without the association, and
1766
+ the site serves from the Origin (see [Limitations](#limitations)).
1767
+
1768
+ ## Web ACLs
1769
+
1770
+ A Distribution can put a WAFv2 web ACL in front of everything it serves. Name the web ACL's ARN in
1771
+ `WebACLId` and the Distribution evaluates it against every request that arrives. A request the web
1772
+ ACL blocks gets 403 from the edge. A request it allows carries on to the cache Behavior and the
1773
+ Origin.
1774
+
1775
+ CloudFront takes its web ACL this way. WAFv2's `AssociateWebACL` covers the regional resource types.
1776
+
1777
+ The web ACL has to be a `CLOUDFRONT` scope one, created in `us-east-1` (see
1778
+ [scopes](https://yulinsim.dev/services/wafv2/#scopes)). A `WebACLId` naming a `REGIONAL` web ACL, or one this
1779
+ simulation never created, is refused with `InvalidWebACLId` at `CreateDistribution` and at
1780
+ `UpdateDistribution`.
1781
+
1782
+ The web ACL decides before any other stage sees the request. A blocked request never reaches a
1783
+ viewer-request CloudFront Function, a cache Behavior, a response headers policy or the Origin.
1784
+
1785
+ A CloudFormation Distribution naming a web ACL this simulation does not hold deploys without one.
1786
+ The `WebACLId` lands on `stack.ignoredProperties` and every request is served, including the ones
1787
+ the web ACL would have decided. A template naming a web ACL from a real account is ordinary, and a
1788
+ site that failed to deploy over its firewall would cost a local dev server and a test suite every
1789
+ request they make. `CreateDistribution` still refuses the same `WebACLId`, as real CloudFront
1790
+ refuses it.
1791
+
1792
+ ```typescript sim-cloudfront-web-acl
1793
+ /**
1794
+ * Blocking a request to a Distribution with a web ACL.
1795
+ */
1796
+
1797
+ import { CreateDistributionCommand } from "@aws-sdk/client-cloudfront";
1798
+ import {
1799
+ CreateBucketCommand,
1800
+ PutBucketPolicyCommand,
1801
+ PutObjectCommand,
1802
+ } from "@aws-sdk/client-s3";
1803
+ import { CreateWebACLCommand } from "@aws-sdk/client-wafv2";
1804
+
1805
+ import { SimAws } from "@kensio/yulin";
1806
+ import { serveSimAws } from "@kensio/yulin/serve";
1807
+
1808
+ const simAws = new SimAws();
1809
+ const srv = await serveSimAws({ simAws });
1810
+
1811
+ try {
1812
+ const simS3 = simAws.s3();
1813
+ await simS3.createBucket(new CreateBucketCommand({ Bucket: "site-bucket" }));
1814
+ await simS3.putBucketPolicy(
1815
+ new PutBucketPolicyCommand({
1816
+ Bucket: "site-bucket",
1817
+ Policy: JSON.stringify({
1818
+ Version: "2012-10-17",
1819
+ Statement: {
1820
+ Effect: "Allow",
1821
+ Principal: "*",
1822
+ Action: "s3:GetObject",
1823
+ Resource: "arn:aws:s3:::site-bucket/*",
1824
+ },
1825
+ }),
1826
+ }),
1827
+ );
1828
+ await simS3.putObject(
1829
+ new PutObjectCommand({
1830
+ Bucket: "site-bucket",
1831
+ Key: "admin/users.html",
1832
+ ContentType: "text/html",
1833
+ Body: "<h1>Users</h1>",
1834
+ }),
1835
+ );
1836
+
1837
+ // A CLOUDFRONT scope web ACL lives in us-east-1, wherever the Distribution
1838
+ // was created from.
1839
+ const acl = await simAws
1840
+ .accountRegionScope(simAws.defaultAccountId, "us-east-1")
1841
+ .wafV2()
1842
+ .createWebAcl(
1843
+ new CreateWebACLCommand({
1844
+ Name: "site-acl",
1845
+ Scope: "CLOUDFRONT",
1846
+ DefaultAction: { Allow: {} },
1847
+ VisibilityConfig: {
1848
+ SampledRequestsEnabled: false,
1849
+ CloudWatchMetricsEnabled: false,
1850
+ MetricName: "site",
1851
+ },
1852
+ Rules: [
1853
+ {
1854
+ Name: "block-admin",
1855
+ Priority: 0,
1856
+ Action: { Block: {} },
1857
+ Statement: {
1858
+ ByteMatchStatement: {
1859
+ FieldToMatch: { UriPath: {} },
1860
+ PositionalConstraint: "STARTS_WITH",
1861
+ SearchString: Buffer.from("/admin"),
1862
+ TextTransformations: [{ Priority: 0, Type: "LOWERCASE" }],
1863
+ },
1864
+ },
1865
+ VisibilityConfig: {
1866
+ SampledRequestsEnabled: false,
1867
+ CloudWatchMetricsEnabled: false,
1868
+ MetricName: "block-admin",
1869
+ },
1870
+ },
1871
+ ],
1872
+ }),
1873
+ );
1874
+
1875
+ const creation = await simAws.cloudFront().createDistribution(
1876
+ new CreateDistributionCommand({
1877
+ DistributionConfig: {
1878
+ CallerReference: "guarded-site",
1879
+ Comment: "Site behind a web ACL",
1880
+ Enabled: true,
1881
+ WebACLId: acl.Summary!.ARN,
1882
+ Origins: {
1883
+ Quantity: 1,
1884
+ Items: [
1885
+ {
1886
+ Id: "site-origin",
1887
+ DomainName: "site-bucket.s3.amazonaws.com",
1888
+ S3OriginConfig: { OriginAccessIdentity: "" },
1889
+ },
1890
+ ],
1891
+ },
1892
+ DefaultCacheBehavior: {
1893
+ TargetOriginId: "site-origin",
1894
+ ViewerProtocolPolicy: "allow-all",
1895
+ },
1896
+ },
1897
+ }),
1898
+ );
1899
+
1900
+ const distroHostname = creation.Distribution!.DomainName!;
1901
+
1902
+ const blocked = await fetch(
1903
+ srv.localUrl(`http://${distroHostname}/admin/users.html`),
1904
+ );
1905
+ console.log(blocked.status); // 403
1906
+
1907
+ // The Bucket still holds the page. The request never got as far as the
1908
+ // Origin to ask for it.
1909
+ } finally {
1910
+ await srv.close();
1911
+ }
1912
+ ```
1913
+
1914
+ See [simulated WAFv2](https://yulinsim.dev/services/wafv2/) for what a rule can inspect and how a blocked request is
1915
+ answered.
1916
+
1917
+ ## Response headers policies
1918
+
1919
+ A response headers policy sets headers on everything a cache Behavior serves. Declare one as
1920
+ `AWS::CloudFront::ResponseHeadersPolicy` and point a Behavior's `ResponseHeadersPolicyId` at it with
1921
+ a `Ref`, which is what CDK's `ResponseHeadersPolicy` construct synthesizes.
1922
+
1923
+ ```typescript sim-cloudfront-response-headers-policy
1924
+ /**
1925
+ * Setting response headers on what a cache Behavior serves.
1926
+ */
1927
+
1928
+ import { PutObjectCommand } from "@aws-sdk/client-s3";
1929
+ import { SimAws } from "@kensio/yulin";
1930
+ import { serveSimAws } from "@kensio/yulin/serve";
1931
+
1932
+ const simAws = new SimAws();
1933
+ const srv = await serveSimAws({ simAws });
1934
+
1935
+ try {
1936
+ const stack = await simAws.cloudFormation().deployTemplate({
1937
+ stackName: "site-stack",
1938
+ template: {
1939
+ Resources: {
1940
+ SiteBucket: {
1941
+ Type: "AWS::S3::Bucket",
1942
+ Properties: {
1943
+ BucketName: "site-bucket",
1944
+ PublicAccessBlockConfiguration: {
1945
+ BlockPublicAcls: true,
1946
+ IgnorePublicAcls: true,
1947
+ },
1948
+ },
1949
+ },
1950
+ // The Origin reads the Bucket anonymously, so the site needs a policy
1951
+ // making it publicly readable.
1952
+ SiteBucketPolicy: {
1953
+ Type: "AWS::S3::BucketPolicy",
1954
+ DependsOn: "SiteBucket",
1955
+ Properties: {
1956
+ Bucket: "site-bucket",
1957
+ PolicyDocument: {
1958
+ Version: "2012-10-17",
1959
+ Statement: {
1960
+ Effect: "Allow",
1961
+ Principal: "*",
1962
+ Action: "s3:GetObject",
1963
+ Resource: "arn:aws:s3:::site-bucket/*",
1964
+ },
1965
+ },
1966
+ },
1967
+ },
1968
+ CacheHeaders: {
1969
+ Type: "AWS::CloudFront::ResponseHeadersPolicy",
1970
+ Properties: {
1971
+ ResponseHeadersPolicyConfig: {
1972
+ Name: "CacheHeaders",
1973
+ CustomHeadersConfig: {
1974
+ Items: [
1975
+ {
1976
+ Header: "Cache-Control",
1977
+ Override: true,
1978
+ Value: "public, max-age=0, must-revalidate",
1979
+ },
1980
+ ],
1981
+ },
1982
+ },
1983
+ },
1984
+ },
1985
+ SiteDistribution: {
1986
+ Type: "AWS::CloudFront::Distribution",
1987
+ DependsOn: ["SiteBucket", "CacheHeaders"],
1988
+ Properties: {
1989
+ DistributionConfig: {
1990
+ DefaultRootObject: "index.html",
1991
+ Origins: [
1992
+ {
1993
+ Id: "SiteOrigin",
1994
+ DomainName: "site-bucket.s3.amazonaws.com",
1995
+ S3OriginConfig: {},
1996
+ },
1997
+ ],
1998
+ DefaultCacheBehavior: {
1999
+ TargetOriginId: "SiteOrigin",
2000
+ ViewerProtocolPolicy: "allow-all",
2001
+ ResponseHeadersPolicyId: { Ref: "CacheHeaders" },
2002
+ },
2003
+ },
2004
+ },
2005
+ },
2006
+ },
2007
+ Outputs: {
2008
+ DistributionDomainName: {
2009
+ Value: { "Fn::GetAtt": ["SiteDistribution", "DomainName"] },
2010
+ },
2011
+ },
2012
+ },
2013
+ });
2014
+
2015
+ await stack.waitForDeployComplete();
2016
+
2017
+ await simAws.s3().putObject(
2018
+ new PutObjectCommand({
2019
+ Bucket: "site-bucket",
2020
+ Key: "index.html",
2021
+ ContentType: "text/html",
2022
+ Body: "<h1>Home</h1>",
2023
+ }),
2024
+ );
2025
+
2026
+ const domainName = stack.output("DistributionDomainName");
2027
+ const response = await fetch(srv.localUrl(`http://${domainName}/`));
2028
+
2029
+ console.log(response.headers.get("cache-control"));
2030
+ } finally {
2031
+ await srv.close();
2032
+ }
2033
+ ```
2034
+
2035
+ Each header in `CustomHeadersConfig` carries an `Override` boolean. With it set, the policy's value
2036
+ replaces one the Origin sent. Without it, the Origin's value is kept and the policy's is dropped. A
2037
+ header the Origin left out is added either way.
2038
+
2039
+ `RemoveHeadersConfig` takes headers away, and is applied before the added ones. A header named in
2040
+ both sections ends up present with the policy's value.
2041
+
2042
+ The policy is applied after a custom error response is fetched and before the viewer-response event,
2043
+ as CloudFront does. An error page carries the policy's headers. A viewer-response Function sees them
2044
+ in `event.response.headers` and can change them, where the Origin answered below 400 and the
2045
+ Function ran at all.
2046
+
2047
+ `SecurityHeadersConfig` is what CDK's `ResponseHeadersPolicy` construct synthesizes from
2048
+ `securityHeadersBehavior`, and every one of its sections is modelled. `ContentSecurityPolicy`,
2049
+ `ContentTypeOptions`, `FrameOptions`, `ReferrerPolicy`, `StrictTransportSecurity` and `XSSProtection`
2050
+ each become the header CloudFront documents for it, honouring the section's own `Override` the same
2051
+ way a `CustomHeadersConfig` item does.
2052
+
2053
+ `ServerTimingHeadersConfig` adds a `Server-Timing` header once `Enabled` is true. `SamplingRate` is
2054
+ ignored. This simulation adds the header to every response. A test asserting on it never depends on
2055
+ chance, and the header's value is a fixed placeholder in place of real Origin timing.
2056
+
2057
+ `CorsConfig` is what CDK's `corsBehavior` synthesizes. CloudFront reflects the viewer request's
2058
+ `Origin` header against `AccessControlAllowOrigins`, in place of sending the list itself. A request
2059
+ naming an Origin the list allows gets the CORS headers the section configures, with the response
2060
+ varying on `Origin` unless the list contains `*`. A request naming one the list omits gets none of
2061
+ them, matching CloudFront, which sends none in preference to a mismatched one.
2062
+ `AccessControlAllowMethods` of `["ALL"]` expands to CloudFront's full method list, and
2063
+ `AccessControlAllowCredentials: false` leaves `Access-Control-Allow-Credentials` off entirely, since
2064
+ a header naming `false` means the same as its absence to a browser.
2065
+
2066
+ `AccessControlAllowMethods`, `AccessControlAllowHeaders` and `AccessControlMaxAgeSec` answer what a
2067
+ preflight asks, and their headers go on a response to an `OPTIONS` request alone. Every other method
2068
+ is answered without them, as CloudFront answers it. `Access-Control-Allow-Origin`,
2069
+ `Access-Control-Allow-Credentials` and `Access-Control-Expose-Headers` come back either way, and so
2070
+ does the `Vary: Origin` a reflected Origin carries. A list left empty sends no header at all, which
2071
+ is how `SimpleCORS` answers with the Origin header by itself.
2072
+
2073
+ An allow-list entry may use the wildcard on its own, meaning every Origin, or as the leftmost
2074
+ subdomain, so `*.example.org` matches `https://site.example.org`. It stands for exactly one label, as
2075
+ a wildcard certificate does, and it leaves `https://deep.site.example.org` unmatched. An entry naming
2076
+ no scheme matches the host whichever scheme the request used. CloudFront allows the wildcard nowhere
2077
+ else, and an entry placing one elsewhere (`example.*`, `test.*.example.org`, `*test.example.org`,
2078
+ `exa*mple.org`) fails the stack.
2079
+
2080
+ `OriginOverride` decides the whole CORS section at once, where the `Override` on a custom or security
2081
+ header decides one header. Without it, an Origin response carrying any CORS header at all, named by
2082
+ the policy or otherwise, keeps every header the section would have set off the response.
2083
+
2084
+ ### Managed policies
2085
+
2086
+ CloudFront's five managed policies are here from the start, under the IDs AWS publishes, and a
2087
+ Behavior names one without a template creating anything. `SecurityHeadersPolicy`
2088
+ (`67f7725c-6f97-4210-82d7-5512b31e9d03`), `SimpleCORS` (`60669652-455b-4ae9-85a4-c4c02393f86c`),
2089
+ `CORS-With-Preflight` (`5cc3b908-e619-4b99-88e5-2cf7f45965bd`), `CORS-and-SecurityHeadersPolicy`
2090
+ (`e61eb60c-9c35-4d20-a928-2b84e02af89c`) and `CORS-with-preflight-and-SecurityHeadersPolicy`
2091
+ (`eaab4381-ed33-4a86-88ca-d9558dc6cd63`) each carry the sections
2092
+ [AWS documents](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-response-headers-policies.html)
2093
+ for it. CDK's `ResponseHeadersPolicy.SECURITY_HEADERS` and its four siblings synthesize those IDs, so
2094
+ a stack reaching for one deploys and serves the headers.
2095
+
2096
+ The managed policies sit in CloudFront's own namespace. A template may create a policy called
2097
+ `SecurityHeadersPolicy` of its own, and deleting that stack leaves the managed one where it was.
2098
+
2099
+ A CloudFormation Distribution whose Behavior names a policy that is neither managed nor created here
2100
+ deploys without one. The `ResponseHeadersPolicyId` lands on `stack.ignoredProperties` under that
2101
+ Behavior, and the Behavior serves every response without the headers the policy would have set. A
2102
+ template naming a policy from a real account, or one another stack created, is ordinary, and a site
2103
+ that failed to deploy over a set of headers would cost a local dev server and a test suite every
2104
+ request they make. One Behavior losing its policy leaves the others holding theirs, and a path
2105
+ Behavior is recorded under its `PathPattern`, the way a skipped Lambda@Edge association is.
2106
+
2107
+ `CreateDistribution` and `UpdateDistribution` still refuse the same ID, as real CloudFront refuses
2108
+ it.
2109
+
2110
+ ## Origin access controls
2111
+
2112
+ An origin access control is how a Distribution authenticates to a private Origin. The Origin then
2113
+ admits the Distribution and nothing else. Declare one as `AWS::CloudFront::OriginAccessControl` and
2114
+ point an Origin's `OriginAccessControlId` at it with a `Ref`, which is what CDK's
2115
+ `S3BucketOrigin.withOriginAccessControl` synthesizes.
2116
+
2117
+ An `OriginAccessControlOriginType` of `s3` signs for an S3 Bucket Origin, and one of `lambda` signs
2118
+ for a Lambda Function URL Origin. The origin type has to match the Origin it is attached to. An `s3`
2119
+ origin access control on a custom Origin, or a `lambda` one on an S3 Origin, fails the Stack when
2120
+ the Distribution is created, as CloudFront refuses it.
2121
+
2122
+ An S3 Origin whose origin access control signs reads its Bucket as the `cloudfront.amazonaws.com`
2123
+ service principal, carrying the Distribution's ARN as `aws:SourceArn`. The Bucket policy is then the
2124
+ whole decision. The Bucket needs a statement granting `s3:GetObject` to that principal, conditioned
2125
+ on the Distribution allowed to read it. That is the policy CDK writes. A condition naming a different
2126
+ Distribution, or an Origin that was never given an origin access control, answers 403.
2127
+
2128
+ ```typescript sim-cloudfront-origin-access-control
2129
+ /**
2130
+ * Serving a private S3 Bucket through an origin access control.
2131
+ */
2132
+
2133
+ import { PutObjectCommand } from "@aws-sdk/client-s3";
2134
+
2135
+ import { SimAws } from "@kensio/yulin";
2136
+ import { serveSimAws } from "@kensio/yulin/serve";
2137
+
2138
+ const simAws = new SimAws();
2139
+ const srv = await serveSimAws({ simAws });
2140
+
2141
+ try {
2142
+ const stack = await simAws.cloudFormation().deployTemplate({
2143
+ stackName: "site-stack",
2144
+ template: {
2145
+ Resources: {
2146
+ SiteBucket: {
2147
+ Type: "AWS::S3::Bucket",
2148
+ Properties: { BucketName: "site-bucket" },
2149
+ },
2150
+ SiteOac: {
2151
+ Type: "AWS::CloudFront::OriginAccessControl",
2152
+ Properties: {
2153
+ OriginAccessControlConfig: {
2154
+ Name: "site-oac",
2155
+ OriginAccessControlOriginType: "s3",
2156
+ SigningBehavior: "always",
2157
+ SigningProtocol: "sigv4",
2158
+ },
2159
+ },
2160
+ },
2161
+ SiteDistribution: {
2162
+ Type: "AWS::CloudFront::Distribution",
2163
+ Properties: {
2164
+ DistributionConfig: {
2165
+ Enabled: true,
2166
+ DefaultRootObject: "index.html",
2167
+ Origins: [
2168
+ {
2169
+ Id: "SiteOrigin",
2170
+ DomainName: "site-bucket.s3.amazonaws.com",
2171
+ S3OriginConfig: {},
2172
+ OriginAccessControlId: { Ref: "SiteOac" },
2173
+ },
2174
+ ],
2175
+ DefaultCacheBehavior: {
2176
+ TargetOriginId: "SiteOrigin",
2177
+ ViewerProtocolPolicy: "allow-all",
2178
+ },
2179
+ },
2180
+ },
2181
+ },
2182
+ // Nothing but this Distribution may read the Bucket, which is what the
2183
+ // condition on the Distribution's ARN says.
2184
+ SiteBucketPolicy: {
2185
+ Type: "AWS::S3::BucketPolicy",
2186
+ Properties: {
2187
+ Bucket: { Ref: "SiteBucket" },
2188
+ PolicyDocument: {
2189
+ Version: "2012-10-17",
2190
+ Statement: [
2191
+ {
2192
+ Effect: "Allow",
2193
+ Principal: { Service: "cloudfront.amazonaws.com" },
2194
+ Action: "s3:GetObject",
2195
+ Resource: "arn:aws:s3:::site-bucket/*",
2196
+ Condition: {
2197
+ StringEquals: {
2198
+ "AWS:SourceArn": {
2199
+ "Fn::Join": [
2200
+ "",
2201
+ [
2202
+ "arn:aws:cloudfront::",
2203
+ { Ref: "AWS::AccountId" },
2204
+ ":distribution/",
2205
+ { Ref: "SiteDistribution" },
2206
+ ],
2207
+ ],
2208
+ },
2209
+ },
2210
+ },
2211
+ },
2212
+ ],
2213
+ },
2214
+ },
2215
+ },
2216
+ },
2217
+ Outputs: {
2218
+ SiteHostname: {
2219
+ Value: { "Fn::GetAtt": ["SiteDistribution", "DomainName"] },
2220
+ },
2221
+ },
2222
+ },
2223
+ });
2224
+
2225
+ await stack.waitForDeployComplete();
2226
+
2227
+ await simAws.s3().putObject(
2228
+ new PutObjectCommand({
2229
+ Bucket: "site-bucket",
2230
+ Key: "index.html",
2231
+ ContentType: "text/html",
2232
+ Body: "<h1>Home</h1>",
2233
+ }),
2234
+ );
2235
+
2236
+ const siteHostname = stack.output("SiteHostname");
2237
+ const home = await fetch(srv.localUrl(`http://${siteHostname}/`));
2238
+
2239
+ console.log(await home.text()); // <h1>Home</h1>
2240
+ } finally {
2241
+ await srv.close();
2242
+ }
2243
+ ```
2244
+
2245
+ The Bucket policy names the Distribution's ARN, and is created after the Distribution. The `Ref`
2246
+ inside `Fn::Join` is the dependency CloudFormation orders the Stack by. The read is settled per
2247
+ request, because the policy deciding it comes into existence after the Distribution does. The Origin
2248
+ works out who it is reading as each time.
2249
+
2250
+ ### A Lambda Function URL Origin
2251
+
2252
+ Putting a Function URL with `AuthType: AWS_IAM` behind a Distribution takes the origin access
2253
+ control with `OriginAccessControlOriginType: lambda`, a custom Origin naming it whose `DomainName`
2254
+ is the Function URL's hostname, and two `AWS::Lambda::Permission` Resources granting
2255
+ `cloudfront.amazonaws.com` for that Distribution. It is the only way to serve a Function URL through
2256
+ CloudFront without leaving the Function URL open to anyone who finds its endpoint.
2257
+
2258
+ Both permissions are needed. One grants `lambda:InvokeFunctionUrl` and the other
2259
+ `lambda:InvokeFunction`, to the same principal with the same `SourceArn`, as
2260
+ [Restrict access to an AWS Lambda function URL origin](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-lambda.html)
2261
+ sets out. CDK's `FunctionUrlOrigin.withOriginAccessControl` writes only the first. A CDK app has to
2262
+ add the second itself:
2263
+
2264
+ ```typescript
2265
+ greeterFunction.addPermission("InvokeFunctionFromCloudFront", {
2266
+ principal: new iam.ServicePrincipal("cloudfront.amazonaws.com"),
2267
+ action: "lambda:InvokeFunction",
2268
+ sourceArn: cdk.Fn.join("", [
2269
+ "arn:",
2270
+ cdk.Aws.PARTITION,
2271
+ ":cloudfront::",
2272
+ cdk.Aws.ACCOUNT_ID,
2273
+ ":distribution/",
2274
+ distribution.distributionId,
2275
+ ]),
2276
+ });
2277
+ ```
2278
+
2279
+ The Origin request is made as the `cloudfront.amazonaws.com` service principal carrying the
2280
+ Distribution's ARN, the same pair an S3 Origin read carries, and the function's resource policy is
2281
+ the whole decision. A Stack missing either permission, or with one naming a different Distribution,
2282
+ deploys and then answers 403 through the Distribution, as the real deployment does. The function is
2283
+ never invoked, and writes no logs to look at either.
2284
+
2285
+ ```typescript sim-cloudfront-function-url-origin-access-control
2286
+ /**
2287
+ * Serving a private Lambda Function URL through an origin access control.
2288
+ */
2289
+
2290
+ import { SimAws } from "@kensio/yulin";
2291
+ import { serveSimAws } from "@kensio/yulin/serve";
2292
+
2293
+ const simAws = new SimAws();
2294
+ const srv = await serveSimAws({ simAws });
2295
+
2296
+ try {
2297
+ const stack = await simAws.cloudFormation().deployTemplate({
2298
+ stackName: "greeter-stack",
2299
+ template: {
2300
+ Resources: {
2301
+ GreeterFunction: {
2302
+ Type: "AWS::Lambda::Function",
2303
+ Properties: {
2304
+ FunctionName: "greeter",
2305
+ Role: "arn:aws:iam::888888888888:role/GreeterRole",
2306
+ Handler: "index.handler",
2307
+ Runtime: "nodejs22.x",
2308
+ Code: {
2309
+ ZipFile:
2310
+ "exports.handler = async () => " +
2311
+ "({ statusCode: 200, body: 'Hello from behind CloudFront' });",
2312
+ },
2313
+ },
2314
+ },
2315
+ GreeterUrl: {
2316
+ Type: "AWS::Lambda::Url",
2317
+ Properties: {
2318
+ TargetFunctionArn: { "Fn::GetAtt": ["GreeterFunction", "Arn"] },
2319
+ AuthType: "AWS_IAM",
2320
+ },
2321
+ },
2322
+ GreeterOac: {
2323
+ Type: "AWS::CloudFront::OriginAccessControl",
2324
+ Properties: {
2325
+ OriginAccessControlConfig: {
2326
+ Name: "greeter-oac",
2327
+ OriginAccessControlOriginType: "lambda",
2328
+ SigningBehavior: "always",
2329
+ SigningProtocol: "sigv4",
2330
+ },
2331
+ },
2332
+ },
2333
+ GreeterDistribution: {
2334
+ Type: "AWS::CloudFront::Distribution",
2335
+ Properties: {
2336
+ DistributionConfig: {
2337
+ Enabled: true,
2338
+ Origins: [
2339
+ {
2340
+ Id: "GreeterOrigin",
2341
+ // An Origin takes a domain name, and the Function URL
2342
+ // attribute is a URL, so the host comes out of it.
2343
+ DomainName: {
2344
+ "Fn::Select": [
2345
+ 2,
2346
+ {
2347
+ "Fn::Split": [
2348
+ "/",
2349
+ { "Fn::GetAtt": ["GreeterUrl", "FunctionUrl"] },
2350
+ ],
2351
+ },
2352
+ ],
2353
+ },
2354
+ CustomOriginConfig: { OriginProtocolPolicy: "https-only" },
2355
+ OriginAccessControlId: { Ref: "GreeterOac" },
2356
+ },
2357
+ ],
2358
+ DefaultCacheBehavior: {
2359
+ TargetOriginId: "GreeterOrigin",
2360
+ ViewerProtocolPolicy: "allow-all",
2361
+ },
2362
+ },
2363
+ },
2364
+ },
2365
+ // Nothing but this Distribution may invoke the Function URL, which is
2366
+ // what the condition on the Distribution's ARN says. Reaching the URL
2367
+ // takes both actions, so leaving either one out is a 403.
2368
+ InvokeFunctionUrlFromCloudFront: {
2369
+ Type: "AWS::Lambda::Permission",
2370
+ Properties: {
2371
+ FunctionName: { "Fn::GetAtt": ["GreeterFunction", "Arn"] },
2372
+ Action: "lambda:InvokeFunctionUrl",
2373
+ Principal: "cloudfront.amazonaws.com",
2374
+ SourceArn: {
2375
+ "Fn::Join": [
2376
+ "",
2377
+ [
2378
+ "arn:aws:cloudfront::",
2379
+ { Ref: "AWS::AccountId" },
2380
+ ":distribution/",
2381
+ { Ref: "GreeterDistribution" },
2382
+ ],
2383
+ ],
2384
+ },
2385
+ },
2386
+ },
2387
+ InvokeFunctionFromCloudFront: {
2388
+ Type: "AWS::Lambda::Permission",
2389
+ Properties: {
2390
+ FunctionName: { "Fn::GetAtt": ["GreeterFunction", "Arn"] },
2391
+ Action: "lambda:InvokeFunction",
2392
+ Principal: "cloudfront.amazonaws.com",
2393
+ SourceArn: {
2394
+ "Fn::Join": [
2395
+ "",
2396
+ [
2397
+ "arn:aws:cloudfront::",
2398
+ { Ref: "AWS::AccountId" },
2399
+ ":distribution/",
2400
+ { Ref: "GreeterDistribution" },
2401
+ ],
2402
+ ],
2403
+ },
2404
+ },
2405
+ },
2406
+ },
2407
+ Outputs: {
2408
+ SiteHostname: {
2409
+ Value: { "Fn::GetAtt": ["GreeterDistribution", "DomainName"] },
2410
+ },
2411
+ },
2412
+ },
2413
+ });
2414
+
2415
+ await stack.waitForDeployComplete();
2416
+
2417
+ const siteHostname = stack.output("SiteHostname");
2418
+ const greeting = await fetch(srv.localUrl(`http://${siteHostname}/greeting`));
2419
+
2420
+ console.log(await greeting.text()); // Hello from behind CloudFront
2421
+ } finally {
2422
+ await srv.close();
2423
+ }
2424
+ ```
2425
+
2426
+ The Function URL is reachable directly as well, on its own endpoint, and it refuses a request that
2427
+ arrives there without the permission the Distribution has. That is the point of the auth type. The
2428
+ endpoint exists, and only the Distribution may use it.
2429
+
2430
+ `SigningBehavior` takes any of `always`, `never` and `no-override`. `always` and `no-override` both
2431
+ sign, since nothing here sends a pre-signed viewer request to an Origin for `no-override` to pass
2432
+ through. `never` turns the origin access control off while leaving it in place, and the Origin is
2433
+ reached anonymously, as an Origin with no origin access control is. An S3 Origin then needs a Bucket
2434
+ policy allowing that, and an `AWS_IAM` Function URL refuses the request outright.
2435
+
2436
+ `Ref` and `Fn::GetAtt` on `Id` both return the ID, so either resolves an Origin's
2437
+ `OriginAccessControlId`. An Origin naming an ID no origin access control holds is refused with
2438
+ `InvalidOriginAccessControl` when the Distribution is created. Tearing the Stack down removes the
2439
+ origin access control, and its name is free again.
2440
+
2441
+ `OriginAccessControlOriginType` must be `s3` or `lambda`, and `SigningProtocol` must be `sigv4`. Any
2442
+ other value fails the Stack by name.
2443
+
2444
+ A CloudFormation template is the only way to make one. There is no `CreateOriginAccessControl`
2445
+ command here.
2446
+
2447
+ #### Posting to a Function URL Origin
2448
+
2449
+ A POST or PUT through an origin access control has to carry the SHA-256 of its body in an
2450
+ `x-amz-content-sha256` header. CloudFront streams the viewer's body on to the Origin without
2451
+ buffering it, and has no hash of its own to sign with. It signs the hash the viewer declared, and
2452
+ `UNSIGNED-PAYLOAD` where the viewer declared none. Lambda supports no unsigned payload, and answers
2453
+ `403` with `The request signature we calculated does not match the signature you provided`. The
2454
+ handler never runs. The declared hash is checked against the body that arrived, and a digest of
2455
+ other bytes is refused the same way.
2456
+
2457
+ A viewer computes the digest of what it is about to send, the way any SigV4 client does:
2458
+
2459
+ ```typescript
2460
+ const body = JSON.stringify({ email: "someone@example.com" });
2461
+ const response = await fetch(`http://${siteHostname}/sign-in`, {
2462
+ method: "POST",
2463
+ body,
2464
+ headers: {
2465
+ "content-type": "application/json",
2466
+ "x-amz-content-sha256": createHash("sha256").update(body).digest("hex"),
2467
+ },
2468
+ });
2469
+ ```
2470
+
2471
+ A GET or a HEAD is left alone. SigV4 hashes an empty payload for a request without a body, and
2472
+ CloudFront can sign one of those on its own. An origin access control with a `SigningBehavior` of
2473
+ `never` signs no Origin request, and states no payload hash for one. A POST through one reaches
2474
+ the Origin anonymously, as it did before.
2475
+
2476
+ AWS documents the requirement on
2477
+ [Restrict access to an AWS Lambda function URL origin](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-lambda.html).
2478
+ A simulated Distribution refuses the request for the same reason a real one does. A form post
2479
+ missing the header fails in a test as well as on the deployment.
2480
+
2481
+ ## Key value stores
2482
+
2483
+ A key value store holds data a CloudFront Function reads at request time. A redirect table or a
2484
+ feature flag can live there instead of being baked into the Function's code.
2485
+
2486
+ AWS splits this across two SDK clients, and so does the simulator. The CloudFront client owns the
2487
+ store, through `CreateKeyValueStoreCommand`, `DescribeKeyValueStoreCommand`,
2488
+ `ListKeyValueStoresCommand`, `UpdateKeyValueStoreCommand` and `DeleteKeyValueStoreCommand`, all
2489
+ addressing a store by name. The key value store client owns the data, through `GetKeyCommand`,
2490
+ `PutKeyCommand`, `DeleteKeyCommand`, `ListKeysCommand`, `UpdateKeysCommand` and its own
2491
+ `DescribeKeyValueStoreCommand`, all addressing a store by ARN.
2492
+
2493
+ Both clients are intercepted by `SimSdk`. Used directly, they are `simAws.cloudFront().keyValueStores()`
2494
+ and `simAws.cloudFrontKeyValueStore()`.
2495
+
2496
+ ```typescript sim-cloudfront-key-value-store
2497
+ /**
2498
+ * Creating a CloudFront key value store and writing keys to it.
2499
+ */
2500
+
2501
+ import { CreateKeyValueStoreCommand } from "@aws-sdk/client-cloudfront";
2502
+ import {
2503
+ DescribeKeyValueStoreCommand,
2504
+ GetKeyCommand,
2505
+ UpdateKeysCommand,
2506
+ } from "@aws-sdk/client-cloudfront-keyvaluestore";
2507
+
2508
+ import { SimAws } from "@kensio/yulin";
2509
+
2510
+ const simAws = new SimAws();
2511
+
2512
+ // The CloudFront client owns the store itself.
2513
+ const created = await simAws
2514
+ .cloudFront()
2515
+ .keyValueStores()
2516
+ .createKeyValueStore(
2517
+ new CreateKeyValueStoreCommand({
2518
+ Name: "redirects",
2519
+ Comment: "Where old paths go",
2520
+ }),
2521
+ );
2522
+
2523
+ const kvsArn = created.KeyValueStore.ARN;
2524
+ const data = simAws.cloudFrontKeyValueStore();
2525
+
2526
+ // The key value store client owns the data, and addresses the store by ARN.
2527
+ // Every write carries an ETag, and it is this API's own: the one the
2528
+ // CloudFront client returned above versions the resource, not the keys.
2529
+ const described = await data.describeKeyValueStore(
2530
+ new DescribeKeyValueStoreCommand({ KvsARN: kvsArn }),
2531
+ );
2532
+
2533
+ const written = await data.updateKeys(
2534
+ new UpdateKeysCommand({
2535
+ KvsARN: kvsArn,
2536
+ IfMatch: described.ETag,
2537
+ Puts: [
2538
+ { Key: "/old-page", Value: "/new-page" },
2539
+ { Key: "/legacy", Value: "/current" },
2540
+ ],
2541
+ }),
2542
+ );
2543
+
2544
+ console.log(written.ItemCount); // 2
2545
+
2546
+ const read = await data.getKey(
2547
+ new GetKeyCommand({ KvsARN: kvsArn, Key: "/old-page" }),
2548
+ );
2549
+
2550
+ console.log(read.Value); // /new-page
2551
+ ```
2552
+
2553
+ A new store is `PROVISIONING` when the command returns and becomes `READY` in the background, as in
2554
+ CloudFront. `await simAws.backgroundTasksComplete()` waits for that.
2555
+
2556
+ ### ETags
2557
+
2558
+ The key value store commands do check `IfMatch`, where the Distribution and Function commands ignore
2559
+ it. Both APIs require it on every write and CloudFront refuses a stale one, which is what stops two
2560
+ writers overwriting each other. A write carrying a stale ETag is refused with `PreconditionFailed`,
2561
+ and a caller has to thread the ETag through the way it does against CloudFront. Each write returns
2562
+ the new ETag for the next one.
2563
+
2564
+ A store has two ETags and they are not interchangeable, as in AWS. Each `DescribeKeyValueStore`
2565
+ returns its own. The CloudFront client's versions the store's configuration, and the key value store
2566
+ client's versions the keys. Writing a key leaves the configuration's ETag where it was, and changing
2567
+ the comment leaves the keys' where it was. A write carrying the other API's ETag is refused, and the
2568
+ message says which of the two it wanted.
2569
+
2570
+ ### Reading a store from a CloudFront Function
2571
+
2572
+ A Function reads its store through `cf`, which it gets from `import cf from "cloudfront"`. That is
2573
+ the one import JS 2.0 has. `cf.kvs()` opens the store the Function is associated with, and its
2574
+ `get`, `exists` and `meta` are all promises. A Function that reads a store is async.
2575
+
2576
+ A Function names the store it may read with `KeyValueStoreAssociations` on its `FunctionConfig`.
2577
+ CloudFront takes at most one, and only on `cloudfront-js-2.0`. An association on the 1.0 runtime is
2578
+ refused, because that runtime has no `cf` to reach a store through.
2579
+
2580
+ ```typescript sim-cloudfront-function-key-value-store
2581
+ /**
2582
+ * Reading a key value store from a CloudFront Function.
2583
+ */
2584
+
2585
+ import {
2586
+ CreateFunctionCommand,
2587
+ CreateKeyValueStoreCommand,
2588
+ } from "@aws-sdk/client-cloudfront";
2589
+ import {
2590
+ DescribeKeyValueStoreCommand,
2591
+ PutKeyCommand,
2592
+ } from "@aws-sdk/client-cloudfront-keyvaluestore";
2593
+
2594
+ import { SimAws } from "@kensio/yulin";
2595
+
2596
+ const simAws = new SimAws();
2597
+
2598
+ const created = await simAws
2599
+ .cloudFront()
2600
+ .keyValueStores()
2601
+ .createKeyValueStore(new CreateKeyValueStoreCommand({ Name: "redirects" }));
2602
+
2603
+ const kvsArn = created.KeyValueStore.ARN;
2604
+ const data = simAws.cloudFrontKeyValueStore();
2605
+
2606
+ const described = await data.describeKeyValueStore(
2607
+ new DescribeKeyValueStoreCommand({ KvsARN: kvsArn }),
2608
+ );
2609
+
2610
+ await data.putKey(
2611
+ new PutKeyCommand({
2612
+ KvsARN: kvsArn,
2613
+ Key: "/old-page",
2614
+ Value: "/new-page",
2615
+ IfMatch: described.ETag,
2616
+ }),
2617
+ );
2618
+
2619
+ // The Function names the store it may read. It gets `cf` from the one import
2620
+ // JS 2.0 has, and the read is awaited, so the handler is async.
2621
+ await simAws.cloudFront().createFunction(
2622
+ new CreateFunctionCommand({
2623
+ Name: "redirect-cff",
2624
+ FunctionConfig: {
2625
+ Comment: "Redirects from a key value store",
2626
+ Runtime: "cloudfront-js-2.0",
2627
+ KeyValueStoreAssociations: {
2628
+ Quantity: 1,
2629
+ Items: [{ KeyValueStoreARN: kvsArn }],
2630
+ },
2631
+ },
2632
+ FunctionCode: Buffer.from(`
2633
+ import cf from "cloudfront";
2634
+
2635
+ async function handler(event) {
2636
+ const request = event.request;
2637
+
2638
+ if (await cf.kvs().exists(request.uri)) {
2639
+ const target = await cf.kvs().get(request.uri);
2640
+
2641
+ return {
2642
+ statusCode: 302,
2643
+ statusDescription: "Found",
2644
+ headers: { location: { value: target } },
2645
+ };
2646
+ }
2647
+
2648
+ return request;
2649
+ }
2650
+ `),
2651
+ }),
2652
+ );
2653
+
2654
+ const cff = simAws.cloudFront().getCloudFrontFunctionByName("redirect-cff");
2655
+
2656
+ const redirected = await cff!.handleViewerRequest(
2657
+ new Request("https://cdn.test/old-page"),
2658
+ );
2659
+
2660
+ console.log((redirected as Response).status); // 302
2661
+ console.log((redirected as Response).headers.get("location")); // /new-page
2662
+ ```
2663
+
2664
+ `get` reads a string by default, and takes `{ format: "json" }` to parse the stored string or
2665
+ `{ format: "bytes" }` for its UTF-8 bytes. A missing key rejects. A Function that wants a default checks
2666
+ `exists` first, as the example does.
2667
+
2668
+ A Function written as a function reference has no import to write, and reads `cf` as a global.
2669
+ Importing `@kensio/yulin/cloudfront/globals` gives that global a type, along with the CloudFront
2670
+ Function event types. Each invocation gets its own `cf` through Node.js asynchronous context, and two
2671
+ Functions associated with different stores read their own even when they run at the same time.
2672
+
2673
+ ### From CloudFormation
2674
+
2675
+ `AWS::CloudFront::KeyValueStore` creates a store, and a Function associates one with
2676
+ `FunctionConfig.KeyValueStoreAssociations`. CloudFormation takes a plain array there, where the SDK
2677
+ takes a `Quantity` and `Items` pair. `Ref` on a key value store is its ARN, and the two fit together
2678
+ directly:
2679
+
2680
+ ```yaml
2681
+ Redirects:
2682
+ Type: AWS::CloudFront::KeyValueStore
2683
+ Properties:
2684
+ Name: redirects
2685
+
2686
+ RedirectFunction:
2687
+ Type: AWS::CloudFront::Function
2688
+ Properties:
2689
+ Name: redirect-cff
2690
+ AutoPublish: true
2691
+ FunctionCode: !Sub "..."
2692
+ FunctionConfig:
2693
+ Comment: Redirects from a key value store
2694
+ Runtime: cloudfront-js-2.0
2695
+ KeyValueStoreAssociations:
2696
+ - KeyValueStoreARN: !Ref Redirects
2697
+ ```
2698
+
2699
+ `Fn::GetAtt` supports `Arn`, `Id` and `Status`. Deleting the Stack deletes the store, after the
2700
+ Functions holding it have gone.
2701
+
2702
+ CDK's `cloudfront.KeyValueStore` and the `keyValueStore` prop on `cloudfront.Function` both deploy.
2703
+ A CDK stack needs no hand-editing.
2704
+
2705
+ `cf.kvs()` refuses when the Function is associated with no store, and refuses an ID belonging to some
2706
+ other store. Handing back an empty store would let a Function that lost its association run to
2707
+ completion and quietly take every default.
2708
+
2709
+ ## Available functionality
2710
+
2711
+ Sim CloudFront currently supports:
2712
+
2713
+ - `CreateDistributionCommand`, `GetDistributionCommand`, `UpdateDistributionCommand` and
2714
+ `DeleteDistributionCommand`
2715
+ - `CreateFunctionCommand` and `DeleteFunctionCommand`
2716
+ - Refusing Function code over CloudFront's 10 KB size limit, with `FunctionSizeLimitExceeded`
2717
+ - Key value stores, through both the CloudFront client and the key value store data client
2718
+ - S3 Origins backed by sim S3 Buckets, reading them as the Bucket policy allows
2719
+ - Custom Origins reaching sim HTTP APIs and sim Lambda Function URLs in process
2720
+ - `CustomHeaders` and `OriginCustomHeaders` on an Origin, for an origin that admits only CloudFront
2721
+ - CloudFront Distribution hostnames such as `distro123.cloudfront.net`
2722
+ - Default cache Behavior and path-based cache Behaviors
2723
+ - `DefaultRootObject` and `CustomErrorResponses`, for static sites and single-page apps
2724
+ - `viewer-request` and `viewer-response` CloudFront Functions, including async ones
2725
+ - Lambda@Edge functions at all four events, through `LambdaFunctionAssociations`
2726
+ - `LambdaFunctionAssociations` on a template's Distribution, and CDK's `edgeLambdas`
2727
+ - CloudFront Functions reading an associated key value store through `cf.kvs()`
2728
+ - `AWS::CloudFront::ResponseHeadersPolicy`, for headers a cache Behavior sets on every response
2729
+ - `AWS::CloudFront::KeyValueStore`, and `KeyValueStoreAssociations` on `AWS::CloudFront::Function`
2730
+ - `AWS::CloudFront::OriginAccessControl`, letting an Origin read a private Bucket as CloudFront
2731
+ - Viewer certificates from sim ACM, including CloudFront's `us-east-1` requirement
2732
+ - `WebACLId`, putting a simulated WAFv2 web ACL in front of everything a Distribution serves
2733
+ - Serving simulated CloudFront traffic on localhost with `serveSimAws`
2734
+
2735
+ The simulator focuses on useful behaviour for tests and local development, ahead of full CloudFront
2736
+ feature parity. Unsupported CloudFront options may be ignored or may throw errors depending on
2737
+ whether the simulator needs them to model the requested behaviour safely.
2738
+
2739
+ ## Limitations
2740
+
2741
+ Where sim CloudFront knowingly behaves differently from AWS:
2742
+
2743
+ - **The origin events run on every request that reaches the Origin.** Real CloudFront runs
2744
+ `origin-request` and `origin-response` on a cache miss, and serves a cache hit without reaching
2745
+ either. Simulated CloudFront holds no cache, and every request that gets as far as the Origin is a
2746
+ miss here. A request a web ACL blocked or a viewer-request function answered reaches neither
2747
+ event, and an `origin-request` function that returns a response leaves the Origin unread with no
2748
+ `origin-response` event after it.
2749
+ - **An Origin keeps its kind and its Bucket through an origin-request function.** Real CloudFront
2750
+ lets a handler hand back `origin.s3` where it was given `origin.custom`, or point an S3 Origin at
2751
+ another Bucket. Both need something a simulated Origin does not hold, the dispatcher that reaches
2752
+ a custom Origin and the Bucket a domain name resolved to when the Distribution was written. Each
2753
+ is refused with the 502 a failed edge function gets, carrying the reason. The domain name, the
2754
+ Origin path and the custom headers are the parts a handler can rewrite.
2755
+ - **A custom Origin reports CloudFront's default connection settings.** `keepaliveTimeout`,
2756
+ `port`, `protocol`, `readTimeout` and `sslProtocols` are what an origin event carries for every
2757
+ custom Origin, whatever `CustomOriginConfig` said, and a handler writing them changes nothing
2758
+ about the fetch. Nothing here opens a socket for them to apply to. The `customHeaders` of an S3
2759
+ Origin are empty for the same kind of reason. An S3 Origin reads its Bucket through GetObject and
2760
+ builds no request for a header to travel on.
2761
+ - **A custom error page is fetched without the origin events.** CloudFront fetches
2762
+ `ResponsePagePath` from the Origin, and an origin function runs for that fetch as it does for any
2763
+ other. Here the page is fetched directly. An `origin-request` function that rewrote the Origin
2764
+ leaves the error page coming from the Behavior's own Origin.
2765
+ - **A CDK `EdgeFunction` outside us-east-1 wants the whole cloud assembly.**
2766
+ `cloudfront.experimental.EdgeFunction` writes the function into a us-east-1 support stack and
2767
+ reads its ARN back through a custom resource in the stack that uses it. `deployCdkOut` deploys
2768
+ both stacks and the read finds the ARN. `deployTemplateFile` on the using stack's template alone
2769
+ deploys one of them, the read finds nothing, and the Distribution goes up without the
2770
+ association, recorded on `stack.ignoredProperties`. `cdk deploy` deploys both either way.
2771
+ - **Nothing is replicated.** Real Lambda@Edge copies the function out to every Region and creates the
2772
+ `AWSServiceRoleForLambdaReplicator` service-linked role to do it. Here the function is invoked
2773
+ where it was created. The trust policy and the `lambda:GetFunction` and `lambda:EnableReplication`
2774
+ permissions a real association needs are still checked, because those are what a first deploy
2775
+ fails on.
2776
+ - **A Lambda@Edge body is never truncated.** CloudFront caps the body it sends a `viewer-request`
2777
+ function and reports `inputTruncated` when it had to cut one. Every simulated body arrives whole
2778
+ and `inputTruncated` is always false, so a test finds out nothing about whether its request would
2779
+ be too large for a real edge function.
2780
+ - **The Origin's status decides whether a viewer-response function runs.** CloudFront skips the
2781
+ viewer-response event once the Origin answers 400 or higher, and both kinds of function are
2782
+ skipped here on that rule. Where the status is replaced further down the pipeline, by a custom
2783
+ error response or by an `origin-response` function, the Origin's own status still decides. AWS
2784
+ documents the restriction against the Origin's status and says nothing about the status something
2785
+ else puts in its place. A Distribution combining the two is where this simulation is guessing. A
2786
+ response an `origin-request` function generated has no Origin status behind it, and its own status
2787
+ stands in.
2788
+ - **CloudFront's disallowed and read-only header lists go unchecked.** Real CloudFront answers 502
2789
+ when an edge function adds `Connection` or edits `Content-Length`. Both kinds of function here
2790
+ write what they like, apart from the viewer-request `host`, which is restored.
2791
+ - **An S3 Origin with no origin access control reads its Bucket anonymously.** That is the unsigned
2792
+ request real CloudFront sends to the S3 REST endpoint without one. The Bucket policy has to make
2793
+ an Object publicly readable for the Distribution to serve it. A legacy
2794
+ `S3OriginConfig.OriginAccessIdentity` is refused by name. It signs the Origin request as a
2795
+ CloudFront canonical user nothing here models, and a Bucket policy written for one would deny the
2796
+ read in silence.
2797
+ - **A signed Origin request carries no signature.** An Origin whose origin access control signs
2798
+ reaches the Origin as the `cloudfront.amazonaws.com` service principal carrying the Distribution's
2799
+ ARN. That pair is what the Bucket policy or the function's resource policy is evaluated against,
2800
+ and no SigV4 signature is computed or checked. A Function URL Origin is told who the request is
2801
+ from at the simulated HTTP boundary, the same way anything else calling into simulated AWS in
2802
+ process says who it is. No other simulated request is signed here either, and the signature
2803
+ itself is beyond what a test can assert on. The payload hash is the one part of a signature that
2804
+ is stated and checked, because a Function URL turns a POST away over it. See
2805
+ [posting to a Function URL Origin](#posting-to-a-function-url-origin).
2806
+ - **An origin access control signs for an S3 or Lambda Function URL Origin only.** CloudFront also
2807
+ signs for MediaStore and MediaPackage V2 Origins, and both are left out. An
2808
+ `OriginAccessControlOriginType` other than `s3` or `lambda`, or a `SigningProtocol` other than
2809
+ `sigv4`, fails the Stack by naming the value. Neither is quietly treated as one of the two.
2810
+ - **An origin access control name is unique, and that is the whole of the checking.** A second one
2811
+ claiming a name is refused with `OriginAccessControlAlreadyExists`, as CloudFront refuses one.
2812
+ - **An origin access control has no command surface.** `CreateOriginAccessControl` and its siblings
2813
+ are absent, and `AWS::CloudFront::OriginAccessControl` is the only way to make one.
2814
+ - **A list's `Quantity` is only checked when it is there.** Every CloudFront list carries a count
2815
+ alongside its items, and a `Quantity` that disagrees with `Items` is refused with
2816
+ `InconsistentQuantities`, as CloudFront refuses it. A list arriving as a plain array, which is the
2817
+ CloudFormation shape, has no count to disagree with, and a template goes unchecked this way. So
2818
+ does a hand-written `{ Items: [...] }` with the count left out. The AWS SDK types make omitting
2819
+ `Quantity` a compile error, so what arrives without one is a different mistake from the one this
2820
+ catches.
2821
+ - **A web ACL a Distribution names has to exist here.** `WebACLId` resolves to a web ACL created in
2822
+ this simulation, and the ARN carries the Account and Region holding it. A managed web ACL, or one
2823
+ from a real account, is refused at create and at update. A CloudFormation Distribution is the
2824
+ exception and deploys without it, recording the property. Deleting a web ACL a Distribution still
2825
+ names leaves the Distribution answering `InvalidWebACLId` on every request, because real WAF
2826
+ refuses that deletion and nothing here tracks the association to refuse it.
2827
+ - **`IfMatch` ETags are ignored on a Distribution or a Function.** `UpdateDistributionCommand`,
2828
+ `DeleteDistributionCommand` and `DeleteFunctionCommand` all accept `IfMatch` and ignore it,
2829
+ leaving both `PreconditionFailed` and `InvalidIfMatchVersion` unused there. A stale ETag there
2830
+ costs a retry. The key value store commands are the exception and do check it, because the data
2831
+ API is built around it, and two writers racing on one store is the case it exists to catch.
2832
+ - **A key value store has no size quota.** CloudFront caps a store's total size and the length of a
2833
+ single key and value, and refuses a write that would exceed either. Nothing here counts against a
2834
+ quota, and `TotalSizeInBytes` is reported without being enforced. A test can find out nothing
2835
+ about whether its data would be too large for a real store.
2836
+ - **A key value store association is fixed once the Function is created.** There is no
2837
+ `UpdateFunction` here, and the store a Function reads is the one it was created with. Delete the
2838
+ Function and create it again to change it.
2839
+ - **A bound handler goes unmeasured.** Function code over CloudFront's 10 KB limit is refused with
2840
+ `FunctionSizeLimitExceeded`, counted on the source as uploaded. A handler passed as a function
2841
+ reference, through `makeCffFunctionCodeInput` or a CloudFormation binding, carries no source to
2842
+ count. The limit reaches only the inline code a real deploy would upload.
2843
+ - **`ImportSource` is unsupported.** `CreateKeyValueStoreCommand` ignores it, and
2844
+ `AWS::CloudFront::KeyValueStore` refuses a Resource carrying one. Nothing here reads an S3 Object
2845
+ as key data, and deploying an empty store would let a test pass against data the deploy should
2846
+ have seeded. Write the keys with `PutKey` or `UpdateKeys`.
2847
+ - **A `Status` Output holds the status at deploy time.** CloudFormation Outputs are resolved once,
2848
+ while a new store is still `PROVISIONING`, so `Fn::GetAtt` on `Status` in an Output reads
2849
+ `PROVISIONING` even though the store goes on to become `READY`. Read the store itself for its
2850
+ current status.
2851
+ - **Key listing is unpaginated.** `ListKeysCommand` and `ListKeyValueStoresCommand` answer with
2852
+ everything and never set a `NextToken` or `NextMarker`, leaving a test with no paging loop to
2853
+ exercise.
2854
+ - **A deletion goes ahead without waiting for the disable to deploy.** Real CloudFront needs the
2855
+ disabled Distribution to reach `Deployed` before it accepts the deletion. Here, `Enabled: false`
2856
+ is enough.
2857
+ - **A disabled Distribution still serves requests.** Real CloudFront answers a disabled Distribution
2858
+ with a 403. Only deleting a Distribution stops it serving here.
2859
+ - **`DeleteFunctionCommand` never answers `FunctionInUse`.** A CloudFront Function is never told
2860
+ that a cache Behavior has taken it up, and every Function is deletable. A Behavior left pointing at a
2861
+ deleted Function runs no Function code.
2862
+ - **A response headers policy name is unique, and that is the whole of the checking.** A second
2863
+ policy claiming a name is refused with `ResponseHeadersPolicyAlreadyExists`, as CloudFront refuses
2864
+ one. The header names and values themselves are stored as written.
2865
+ - **A response headers policy has no command surface.** `CreateResponseHeadersPolicy` and its
2866
+ siblings are absent, and `AWS::CloudFront::ResponseHeadersPolicy` is the only way to make one.
2867
+ - **`ServerTimingHeadersConfig` always adds the header once enabled.** `SamplingRate` decides what
2868
+ share of real responses carry `Server-Timing`. This simulation adds it to every response once
2869
+ `Enabled` is true. A test asserting on it never depends on chance. The header's value is a
2870
+ fixed placeholder, since nothing here measures an Origin fetch the way CloudFront's edge does.
2871
+ - **`CachePolicyId` and `OriginRequestPolicyId` are accepted and ignored.** Sim CloudFront models no
2872
+ edge caching. A Behavior's cache policy, including an AWS managed policy such as
2873
+ `CachingOptimized`, is left unvalidated and unapplied to TTLs and the cache key. Every request
2874
+ reaches the Origin, whatever the policy would have cached on real CloudFront.