@kensio/yulin 1.20.15 → 1.20.17

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 (355) 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-ambient-caller.d.ts +23 -0
  9. package/dist/service/aws/caller/sim-aws-ambient-caller.js +12 -0
  10. package/dist/service/aws/caller/sim-aws-caller-resolver.d.ts +39 -6
  11. package/dist/service/aws/caller/sim-aws-caller-resolver.js +32 -30
  12. package/dist/service/aws/caller/sim-aws-caller.d.ts +9 -0
  13. package/dist/service/aws/caller/sim-aws-resolved-caller.d.ts +19 -0
  14. package/dist/service/aws/caller/sim-aws-resolved-caller.js +26 -0
  15. package/dist/service/aws/factory/sim-aws-account-region-service-builder.d.ts +12 -0
  16. package/dist/service/aws/factory/sim-aws-account-region-service-builder.js +6 -0
  17. package/dist/service/aws/factory/sim-aws-account-service-cache.d.ts +12 -0
  18. package/dist/service/aws/factory/sim-aws-account-service-cache.js +9 -0
  19. package/dist/service/aws/factory/sim-aws-registered-service-builder.js +3 -1
  20. package/dist/service/aws/factory/sim-aws-service-factory.d.ts +5 -0
  21. package/dist/service/aws/factory/sim-aws-service-factory.js +14 -0
  22. package/dist/service/aws/sim-aws-properties.d.ts +15 -0
  23. package/dist/service/aws/sim-aws.js +2 -1
  24. package/dist/service/cloudformation/command/create-stack/create-stack.handler.d.ts +4 -0
  25. package/dist/service/cloudformation/command/create-stack/create-stack.handler.js +4 -1
  26. package/dist/service/cloudformation/deploy/sim-cfn-cdk-out-deployer.js +2 -1
  27. package/dist/service/cloudformation/deploy/sim-cfn-cdk-out-plan.d.ts +11 -2
  28. package/dist/service/cloudformation/deploy/sim-cfn-cdk-out-plan.js +2 -1
  29. package/dist/service/cloudformation/deploy/sim-cfn-cdk-out-stack-options.d.ts +6 -0
  30. package/dist/service/cloudformation/deploy/sim-cfn-template-deployer.d.ts +11 -0
  31. package/dist/service/cloudformation/deploy/sim-cfn-template-deployer.js +17 -15
  32. package/dist/service/cloudformation/deploy/sim-cfn-template-file-loader.d.ts +12 -0
  33. package/dist/service/cloudformation/deploy/sim-cfn-template-file-loader.js +2 -1
  34. package/dist/service/cloudformation/resource/cfn/cloudfront/sim-cloudfront-cache-policy-cfn.d.ts +23 -0
  35. package/dist/service/cloudformation/resource/cfn/cloudfront/sim-cloudfront-cache-policy-cfn.js +30 -0
  36. package/dist/service/cloudformation/resource/cfn/cloudfront/sim-cloudfront-cfn-value-adapter.js +8 -0
  37. package/dist/service/cloudformation/resource/name/sim-cfn-generated-resource-name.d.ts +29 -12
  38. package/dist/service/cloudformation/resource/name/sim-cfn-generated-resource-name.js +51 -20
  39. package/dist/service/cloudformation/stack/deploy/sim-cfn-resource-order.d.ts +19 -0
  40. package/dist/service/cloudformation/stack/deploy/sim-cfn-resource-order.js +6 -0
  41. package/dist/service/cloudformation/stack/deploy/sim-cfn-stack-resource-batch-creator.d.ts +7 -0
  42. package/dist/service/cloudformation/stack/deploy/sim-cfn-stack-resource-batch-creator.js +9 -2
  43. package/dist/service/cloudformation/stack/deploy/sim-cfn-stack-resource-creator.d.ts +2 -0
  44. package/dist/service/cloudformation/stack/deploy/sim-cfn-stack-resource-creator.js +2 -1
  45. package/dist/service/cloudformation/stack/sim-cfn-stack-resource-operations.d.ts +6 -0
  46. package/dist/service/cloudformation/stack/sim-cfn-stack-resource-operations.js +4 -1
  47. package/dist/service/cloudformation/stack/sim-cfn-stack.js +2 -1
  48. package/dist/service/cloudformation/stack/sim-cfn-stack.type.d.ts +6 -0
  49. package/dist/service/cloudfront/behaviour/sim-cloud-front-behavior.d.ts +5 -0
  50. package/dist/service/cloudfront/cache-policy/sim-cf-cache-policy-registry.d.ts +37 -0
  51. package/dist/service/cloudfront/cache-policy/sim-cf-cache-policy-registry.js +50 -0
  52. package/dist/service/cloudfront/cache-policy/sim-cf-cache-policy.d.ts +25 -0
  53. package/dist/service/cloudfront/cache-policy/sim-cf-cache-policy.js +21 -0
  54. package/dist/service/cloudfront/cache-policy/sim-cf-managed-cache-policies.d.ts +28 -0
  55. package/dist/service/cloudfront/cache-policy/sim-cf-managed-cache-policies.js +48 -0
  56. package/dist/service/cloudfront/cff/function-code-input/cff-function-code-input.d.ts +8 -0
  57. package/dist/service/cloudfront/cff/function-code-input/cff-function-code-input.js +13 -0
  58. package/dist/service/cloudfront/cff/sim-cf-function-commands.d.ts +52 -0
  59. package/dist/service/cloudfront/cff/sim-cf-function-commands.js +51 -0
  60. package/dist/service/cloudfront/cff/sim-cff-configuration.d.ts +41 -0
  61. package/dist/service/cloudfront/cff/sim-cff-configuration.js +34 -0
  62. package/dist/service/cloudfront/cff/sim-cff-stage.d.ts +23 -0
  63. package/dist/service/cloudfront/cff/sim-cff-stage.js +30 -0
  64. package/dist/service/cloudfront/cff/sim-cloudfront-function.d.ts +6 -1
  65. package/dist/service/cloudfront/cff/sim-cloudfront-function.js +6 -0
  66. package/dist/service/cloudfront/cfn/cache-policy/sim-cfn-cf-cache-policy-config.d.ts +27 -0
  67. package/dist/service/cloudfront/cfn/cache-policy/sim-cfn-cf-cache-policy-config.js +42 -0
  68. package/dist/service/cloudfront/cfn/cache-policy/sim-cfn-cf-cache-policy-creator.d.ts +24 -0
  69. package/dist/service/cloudfront/cfn/cache-policy/sim-cfn-cf-cache-policy-creator.js +32 -0
  70. package/dist/service/cloudfront/cfn/distro/{sim-cfn-cf-distro-response-headers-policy.d.ts → sim-cfn-cf-distro-behavior-policies.d.ts} +8 -7
  71. package/dist/service/cloudfront/cfn/distro/{sim-cfn-cf-distro-response-headers-policy.js → sim-cfn-cf-distro-behavior-policies.js} +11 -37
  72. package/dist/service/cloudfront/cfn/distro/sim-cfn-cf-distro-creator.d.ts +6 -5
  73. package/dist/service/cloudfront/cfn/distro/sim-cfn-cf-distro-creator.js +8 -7
  74. package/dist/service/cloudfront/cfn/distro/sim-cfn-cf-distro-policy-drops.d.ts +23 -0
  75. package/dist/service/cloudfront/cfn/distro/sim-cfn-cf-distro-policy-drops.js +50 -0
  76. package/dist/service/cloudfront/cfn/sim-cfn-cloudfront-resource-factory.d.ts +1 -0
  77. package/dist/service/cloudfront/cfn/sim-cfn-cloudfront-resource-factory.js +10 -0
  78. package/dist/service/cloudfront/command/create-distribution/create-distribution.command.d.ts +1 -0
  79. package/dist/service/cloudfront/command/create-distribution/create-distribution.handler.d.ts +2 -0
  80. package/dist/service/cloudfront/command/create-function/create-function.handler.js +7 -1
  81. package/dist/service/cloudfront/command/function/sim-cf-describe-function.d.ts +22 -0
  82. package/dist/service/cloudfront/command/function/sim-cf-describe-function.js +32 -0
  83. package/dist/service/cloudfront/command/function/sim-cf-function-access.d.ts +48 -0
  84. package/dist/service/cloudfront/command/function/sim-cf-function-access.js +48 -0
  85. package/dist/service/cloudfront/command/function/sim-cf-function-command.types.d.ts +97 -0
  86. package/dist/service/cloudfront/command/function/sim-cf-function-command.types.js +10 -0
  87. package/dist/service/cloudfront/command/function/sim-cf-function-lookup.d.ts +12 -0
  88. package/dist/service/cloudfront/command/function/sim-cf-function-lookup.js +21 -0
  89. package/dist/service/cloudfront/command/function/sim-cf-function-summary.d.ts +11 -0
  90. package/dist/service/cloudfront/command/function/sim-cf-function-summary.js +30 -0
  91. package/dist/service/cloudfront/command/function/sim-cf-get-function.d.ts +23 -0
  92. package/dist/service/cloudfront/command/function/sim-cf-get-function.js +33 -0
  93. package/dist/service/cloudfront/command/function/sim-cf-list-functions.d.ts +22 -0
  94. package/dist/service/cloudfront/command/function/sim-cf-list-functions.js +38 -0
  95. package/dist/service/cloudfront/command/update-distribution/update-distribution.handler.d.ts +2 -0
  96. package/dist/service/cloudfront/distribution/configurator/sim-cf-behavior-cache-policy.d.ts +18 -0
  97. package/dist/service/cloudfront/distribution/configurator/sim-cf-behavior-cache-policy.js +30 -0
  98. package/dist/service/cloudfront/distribution/configurator/sim-cf-behavior-policies.d.ts +28 -0
  99. package/dist/service/cloudfront/distribution/configurator/sim-cf-behavior-policies.js +41 -0
  100. package/dist/service/cloudfront/distribution/configurator/sim-cf-behavior-properties.d.ts +2 -2
  101. package/dist/service/cloudfront/distribution/configurator/sim-cf-behavior-properties.js +6 -3
  102. package/dist/service/cloudfront/distribution/configurator/sim-cf-behavior-response-headers-policy.d.ts +0 -10
  103. package/dist/service/cloudfront/distribution/configurator/sim-cf-behavior-response-headers-policy.js +0 -19
  104. package/dist/service/cloudfront/distribution/configurator/sim-cf-distribution-configurator.factory.d.ts +2 -0
  105. package/dist/service/cloudfront/distribution/configurator/sim-cf-distribution-configurator.factory.js +4 -2
  106. package/dist/service/cloudfront/distribution/configurator/sim-cloud-front-behavior-configurator.d.ts +3 -3
  107. package/dist/service/cloudfront/distribution/configurator/sim-cloud-front-behavior-configurator.js +5 -5
  108. package/dist/service/cloudfront/distribution/configurator/sim-cloud-front-distribution-configurator.d.ts +3 -3
  109. package/dist/service/cloudfront/distribution/configurator/sim-cloud-front-distribution-configurator.js +4 -4
  110. package/dist/service/cloudfront/distribution/sim-cf-distribution-configuration-state.d.ts +3 -1
  111. package/dist/service/cloudfront/distribution/sim-cf-distribution-reconfigurer.d.ts +2 -0
  112. package/dist/service/cloudfront/error/sim-cloudfront.error.d.ts +21 -0
  113. package/dist/service/cloudfront/error/sim-cloudfront.error.js +25 -0
  114. package/dist/service/cloudfront/key-value-store/sim-cf-key-value-store-access.js +2 -10
  115. package/dist/service/cloudfront/key-value-store/sim-cf-key-value-store.js +2 -1
  116. package/dist/service/cloudfront/sdk/sim-cloudfront-sdk-command-router.js +12 -0
  117. package/dist/service/cloudfront/sim-cf-authorize.d.ts +16 -0
  118. package/dist/service/cloudfront/sim-cf-authorize.js +19 -0
  119. package/dist/service/cloudfront/sim-cf-etag.d.ts +7 -0
  120. package/dist/service/cloudfront/sim-cf-etag.js +10 -0
  121. package/dist/service/cloudfront/sim-cloudfront-commands.d.ts +8 -12
  122. package/dist/service/cloudfront/sim-cloudfront-commands.js +8 -17
  123. package/dist/service/cloudfront/sim-cloudfront-policies.d.ts +43 -0
  124. package/dist/service/cloudfront/sim-cloudfront-policies.js +53 -0
  125. package/dist/service/cloudfront/sim-cloudfront.d.ts +13 -17
  126. package/dist/service/cloudfront/sim-cloudfront.js +22 -25
  127. package/dist/service/ecr/authorize/sim-ecr-authorizer.d.ts +31 -0
  128. package/dist/service/ecr/authorize/sim-ecr-authorizer.js +38 -0
  129. package/dist/service/ecr/cfn/repository/sim-cfn-ecr-repository-creator.d.ts +11 -2
  130. package/dist/service/ecr/cfn/repository/sim-cfn-ecr-repository-creator.js +11 -2
  131. package/dist/service/ecr/cfn/sim-ecr-cfn-resource-factory.d.ts +4 -2
  132. package/dist/service/ecr/cfn/sim-ecr-cfn-resource-factory.js +8 -9
  133. package/dist/service/ecr/cfn/sim-ecr-cfn-unsupported-resource.d.ts +7 -0
  134. package/dist/service/ecr/cfn/sim-ecr-cfn-unsupported-resource.js +9 -0
  135. package/dist/service/ecr/sim-ecr.d.ts +7 -0
  136. package/dist/service/ecr/sim-ecr.js +16 -4
  137. package/dist/service/ecs/command/authorize/sim-ecs-authorizer.d.ts +12 -0
  138. package/dist/service/ecs/command/authorize/sim-ecs-authorizer.js +21 -0
  139. package/dist/service/ecs/command/register-task-definition/register-task-definition.handler.js +1 -0
  140. package/dist/service/ecs/sim-ecs-service-principal.d.ts +8 -0
  141. package/dist/service/ecs/sim-ecs-service-principal.js +8 -0
  142. package/dist/service/eventbridge/command/authorize/sim-event-bridge-authorizer.d.ts +12 -0
  143. package/dist/service/eventbridge/command/authorize/sim-event-bridge-authorizer.js +21 -0
  144. package/dist/service/eventbridge/command/rule/sim-event-bridge-rule-access.d.ts +13 -0
  145. package/dist/service/eventbridge/command/rule/sim-event-bridge-rule-access.js +9 -0
  146. package/dist/service/eventbridge/command/target/sim-event-bridge-put-targets.js +1 -0
  147. package/dist/service/firehose/command/authorize/sim-firehose-authorizer.d.ts +12 -0
  148. package/dist/service/firehose/command/authorize/sim-firehose-authorizer.js +20 -0
  149. package/dist/service/firehose/command/sim-firehose-delivery-stream-access.d.ts +4 -0
  150. package/dist/service/firehose/command/sim-firehose-delivery-stream-access.js +6 -0
  151. package/dist/service/firehose/command/stream/sim-firehose-create-delivery-stream.js +3 -1
  152. package/dist/service/firehose/sim-firehose-service-principal.d.ts +8 -0
  153. package/dist/service/firehose/sim-firehose-service-principal.js +8 -0
  154. package/dist/service/firehose/source/sim-firehose-source.d.ts +5 -0
  155. package/dist/service/firehose/source/sim-firehose-source.js +5 -0
  156. package/dist/service/glue/cfn/database/sim-cfn-glue-database-properties.js +2 -2
  157. package/dist/service/glue/cfn/sim-cfn-glue-generated-name.d.ts +10 -0
  158. package/dist/service/glue/cfn/sim-cfn-glue-generated-name.js +25 -0
  159. package/dist/service/glue/cfn/table/sim-cfn-glue-table-properties.js +2 -3
  160. package/dist/service/iam/authorize/allow/sim-iam-allow-requirement.d.ts +33 -2
  161. package/dist/service/iam/authorize/allow/sim-iam-allow-requirement.js +33 -2
  162. package/dist/service/iam/authorize/allow/sim-iam-allow-statements.d.ts +4 -3
  163. package/dist/service/iam/authorize/allow/sim-iam-allow-statements.js +9 -3
  164. package/dist/service/iam/authorize/context/sim-iam-auth-z-allow-requirement.d.ts +14 -5
  165. package/dist/service/iam/authorize/context/sim-iam-auth-z-allow-requirement.js +16 -12
  166. package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.d.ts +36 -7
  167. package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.js +22 -11
  168. package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.d.ts +10 -71
  169. package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.js +7 -2
  170. package/dist/service/iam/authorize/context/sim-iam-auth-z-input.d.ts +87 -0
  171. package/dist/service/iam/authorize/context/sim-iam-auth-z-input.js +1 -0
  172. package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.d.ts +21 -3
  173. package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.js +34 -10
  174. package/dist/service/iam/authorize/match/sim-iam-principal-match.d.ts +29 -8
  175. package/dist/service/iam/authorize/match/sim-iam-principal-match.js +38 -13
  176. package/dist/service/iam/authorize/pass-role/sim-iam-pass-role-authorizer.d.ts +74 -0
  177. package/dist/service/iam/authorize/pass-role/sim-iam-pass-role-authorizer.js +92 -0
  178. package/dist/service/iam/authorize/sim-iam-account-auth-z.d.ts +21 -5
  179. package/dist/service/iam/authorize/sim-iam-account-auth-z.js +7 -3
  180. package/dist/service/iam/authorize/sim-iam-authorizer.d.ts +5 -2
  181. package/dist/service/iam/authorize/sim-iam-authorizer.js +3 -1
  182. package/dist/service/iam/authorize/sim-iam-inter-service-auth-z.d.ts +1 -1
  183. package/dist/service/iam/authorize/sim-iam-wildcard.js +39 -4
  184. package/dist/service/iam/cfn/managed-policy/sim-cfn-iam-managed-policy-creator.js +2 -1
  185. package/dist/service/iam/cfn/name/sim-cfn-iam-generated-name.d.ts +16 -0
  186. package/dist/service/iam/cfn/name/sim-cfn-iam-generated-name.js +36 -0
  187. package/dist/service/iam/cfn/role/sim-cfn-iam-role-properties-parser.js +9 -8
  188. package/dist/service/iam/cfn/sim-cfn-iam-optional-string.d.ts +17 -0
  189. package/dist/service/iam/cfn/sim-cfn-iam-optional-string.js +17 -0
  190. package/dist/service/iam/cfn/user/sim-cfn-iam-user-properties-parser.js +4 -8
  191. package/dist/service/iam/command/policy/create-policy/create-policy-input-resolver.js +2 -0
  192. package/dist/service/iam/command/policy/put-role-policy/put-role-policy.handler.js +5 -0
  193. package/dist/service/iam/command/policy/put-user-policy/put-user-policy.handler.js +5 -0
  194. package/dist/service/iam/command/role/create-role/create-role.handler.js +4 -1
  195. package/dist/service/iam/error/sim-iam.error.d.ts +11 -0
  196. package/dist/service/iam/error/sim-iam.error.js +13 -0
  197. package/dist/service/iam/index.d.ts +1 -0
  198. package/dist/service/iam/policy/sim-iam-policy-document-of-size.d.ts +18 -0
  199. package/dist/service/iam/policy/sim-iam-policy-document-of-size.js +41 -0
  200. package/dist/service/iam/role/sim-iam-deploy-role.d.ts +56 -0
  201. package/dist/service/iam/role/sim-iam-deploy-role.js +50 -0
  202. package/dist/service/iam/role/sim-iam-role-with-policy.factory.d.ts +11 -0
  203. package/dist/service/iam/role/sim-iam-role-with-policy.factory.js +4 -2
  204. package/dist/service/iam/sim-iam-account-parts.d.ts +16 -0
  205. package/dist/service/iam/sim-iam-account-parts.js +2 -0
  206. package/dist/service/iam/sim-iam.d.ts +12 -2
  207. package/dist/service/iam/sim-iam.js +12 -0
  208. package/dist/service/iam/validate/size/sim-iam-policy-document-size.d.ts +57 -0
  209. package/dist/service/iam/validate/size/sim-iam-policy-document-size.js +71 -0
  210. package/dist/service/kms/command/authorize/sim-kms-authorizer.d.ts +3 -1
  211. package/dist/service/kms/command/authorize/sim-kms-authorizer.js +4 -1
  212. package/dist/service/kms/command/sim-kms-request-options.d.ts +11 -0
  213. package/dist/service/kms/key/sim-kms-key-policy.d.ts +5 -4
  214. package/dist/service/kms/key/sim-kms-key-policy.js +4 -3
  215. package/dist/service/lambda/cfn/function/sim-cfn-lambda-function-properties-parser.d.ts +2 -2
  216. package/dist/service/lambda/cfn/function/sim-cfn-lambda-function-properties-parser.js +4 -3
  217. package/dist/service/lambda/cfn/function/sim-cfn-lambda-generated-function-name.d.ts +10 -0
  218. package/dist/service/lambda/cfn/function/sim-cfn-lambda-generated-function-name.js +25 -0
  219. package/dist/service/lambda/command/authorize/sim-lambda-resource-policies.d.ts +1 -1
  220. package/dist/service/lambda/command/create-function/create-function-authorizer.d.ts +9 -2
  221. package/dist/service/lambda/command/create-function/create-function-authorizer.js +16 -2
  222. package/dist/service/lambda/command/create-function/create-function.handler.js +1 -1
  223. package/dist/service/lambda/command/update-function-configuration/update-function-configuration-authorizer.d.ts +7 -2
  224. package/dist/service/lambda/command/update-function-configuration/update-function-configuration-authorizer.js +14 -2
  225. package/dist/service/lambda/command/update-function-configuration/update-function-configuration.handler.js +1 -1
  226. package/dist/service/lambda/function/logging/sim-lambda-function-logging.d.ts +10 -1
  227. package/dist/service/lambda/function/logging/sim-lambda-function-logging.js +25 -1
  228. package/dist/service/lambda/function/logging/sim-lambda-invoke-error-log.d.ts +13 -0
  229. package/dist/service/lambda/function/logging/sim-lambda-invoke-error-log.js +18 -0
  230. package/dist/service/lambda/sim-lambda-service-principal.d.ts +7 -0
  231. package/dist/service/lambda/sim-lambda-service-principal.js +7 -0
  232. package/dist/service/logs/cfn/delivery/sim-cfn-delivery-authorization.d.ts +46 -0
  233. package/dist/service/logs/cfn/delivery/sim-cfn-delivery-authorization.js +49 -0
  234. package/dist/service/logs/cfn/delivery/sim-cfn-delivery-creator.d.ts +5 -0
  235. package/dist/service/logs/cfn/delivery/sim-cfn-delivery-creator.js +6 -0
  236. package/dist/service/logs/cfn/delivery/sim-cfn-delivery-destination-creator.d.ts +5 -0
  237. package/dist/service/logs/cfn/delivery/sim-cfn-delivery-destination-creator.js +6 -0
  238. package/dist/service/logs/cfn/delivery/sim-cfn-delivery-source-creator.d.ts +5 -0
  239. package/dist/service/logs/cfn/delivery/sim-cfn-delivery-source-creator.js +6 -0
  240. package/dist/service/logs/cfn/group/sim-cfn-log-group-creator.d.ts +9 -2
  241. package/dist/service/logs/cfn/group/sim-cfn-log-group-creator.js +34 -5
  242. package/dist/service/logs/cfn/sim-logs-cfn-resource-deleter.js +1 -1
  243. package/dist/service/logs/cfn/sim-logs-cfn-resource-factory.d.ts +4 -0
  244. package/dist/service/logs/cfn/sim-logs-cfn-resource-factory.js +9 -4
  245. package/dist/service/logs/sim-logs-commands.d.ts +3 -0
  246. package/dist/service/logs/sim-logs-commands.js +4 -0
  247. package/dist/service/logs/sim-logs.js +6 -1
  248. package/dist/service/s3/cfn/bucket/sim-cfn-s3-bucket-creator.d.ts +2 -2
  249. package/dist/service/s3/cfn/bucket/sim-cfn-s3-bucket-creator.js +4 -3
  250. package/dist/service/s3/cfn/bucket/sim-cfn-s3-bucket-generated-name.d.ts +12 -0
  251. package/dist/service/s3/cfn/bucket/sim-cfn-s3-bucket-generated-name.js +22 -0
  252. package/dist/service/s3/command/authorize/sim-s3-bucket-resource-policies.d.ts +1 -1
  253. package/dist/service/scheduler/command/authorize/sim-scheduler-authorizer.d.ts +10 -1
  254. package/dist/service/scheduler/command/authorize/sim-scheduler-authorizer.js +19 -1
  255. package/dist/service/scheduler/command/schedule/sim-scheduler-create-schedule.js +1 -0
  256. package/dist/service/scheduler/command/schedule/sim-scheduler-schedule-access.d.ts +8 -0
  257. package/dist/service/scheduler/command/schedule/sim-scheduler-schedule-access.js +9 -0
  258. package/dist/service/scheduler/command/schedule/sim-scheduler-update-schedule.js +1 -0
  259. package/dist/service/secretsmanager/cfn/secret/sim-cfn-secrets-manager-generated-name.d.ts +15 -0
  260. package/dist/service/secretsmanager/cfn/secret/sim-cfn-secrets-manager-generated-name.js +22 -0
  261. package/dist/service/secretsmanager/cfn/secret/sim-cfn-secrets-manager-secret-properties.d.ts +4 -10
  262. package/dist/service/secretsmanager/cfn/secret/sim-cfn-secrets-manager-secret-properties.js +13 -31
  263. package/dist/service/secretsmanager/cfn/secret/sim-cfn-secrets-manager-secret-value.d.ts +17 -0
  264. package/dist/service/secretsmanager/cfn/secret/sim-cfn-secrets-manager-secret-value.js +30 -0
  265. package/dist/service/secretsmanager/secret/sim-secrets-manager-secret-name.d.ts +6 -0
  266. package/dist/service/secretsmanager/secret/sim-secrets-manager-secret-name.js +8 -3
  267. package/dist/service/sns/command/authorize/sim-sns-topic-resource-policies.d.ts +1 -1
  268. package/dist/service/sqs/command/authorize/sim-sqs-queue-resource-policies.d.ts +1 -1
  269. package/dist/service/ssm/cfn/parameter/sim-cfn-ssm-generated-parameter-name.d.ts +16 -0
  270. package/dist/service/ssm/cfn/parameter/sim-cfn-ssm-generated-parameter-name.js +23 -0
  271. package/dist/service/ssm/cfn/parameter/sim-cfn-ssm-parameter-properties.d.ts +2 -4
  272. package/dist/service/ssm/cfn/parameter/sim-cfn-ssm-parameter-properties.js +5 -5
  273. package/dist/service/ssm/parameter/sim-ssm-kms-crypto.d.ts +6 -4
  274. package/dist/service/ssm/parameter/sim-ssm-kms-crypto.js +4 -4
  275. package/dist/service/ssm/parameter/sim-ssm-parameter-arn.d.ts +17 -0
  276. package/dist/service/ssm/parameter/sim-ssm-parameter-arn.js +20 -0
  277. package/dist/service/ssm/parameter/sim-ssm-parameter-kms.d.ts +15 -9
  278. package/dist/service/ssm/parameter/sim-ssm-parameter-kms.js +20 -10
  279. package/dist/service/ssm/parameter/sim-ssm-parameter-name.js +3 -12
  280. package/dist/service/stepfunctions/command/authorize/sim-step-functions-authorizer.d.ts +41 -0
  281. package/dist/service/stepfunctions/command/authorize/sim-step-functions-authorizer.js +64 -0
  282. package/dist/service/stepfunctions/command/machine/sim-state-machine-create.d.ts +5 -2
  283. package/dist/service/stepfunctions/command/machine/sim-state-machine-create.js +6 -2
  284. package/dist/service/stepfunctions/command/machine/sim-state-machine-delete.d.ts +26 -0
  285. package/dist/service/stepfunctions/command/machine/sim-state-machine-delete.js +29 -0
  286. package/dist/service/stepfunctions/command/machine/sim-state-machine-lookup.d.ts +7 -0
  287. package/dist/service/stepfunctions/command/machine/sim-state-machine-lookup.js +12 -3
  288. package/dist/service/stepfunctions/command/machine/sim-state-machine-update.d.ts +19 -0
  289. package/dist/service/stepfunctions/command/machine/{sim-state-machine-writes.js → sim-state-machine-update.js} +3 -14
  290. package/dist/service/stepfunctions/sim-step-functions.d.ts +8 -2
  291. package/dist/service/stepfunctions/sim-step-functions.js +26 -9
  292. package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.d.ts +9 -2
  293. package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.js +2 -2
  294. package/dist/service/sts/auth-z/assume-role-source-account-auth-z.d.ts +10 -4
  295. package/dist/service/sts/auth-z/assume-role-source-account-auth-z.js +8 -7
  296. package/dist/service/sts/auth-z/assume-role-target-auth-z.d.ts +2 -2
  297. package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.d.ts +6 -2
  298. package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.js +14 -11
  299. package/dist/service/sts/command/assume-role/assume-role.handler.d.ts +10 -1
  300. package/dist/service/sts/command/assume-role/assume-role.handler.js +6 -2
  301. package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.d.ts +10 -1
  302. package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.js +5 -1
  303. package/dist/service/sts/service-role/sim-service-role.js +5 -1
  304. package/dist/service/sts/sim-sts.d.ts +18 -1
  305. package/dist/service/sts/sim-sts.js +8 -0
  306. package/dist/util/memo/bounded-memo.d.ts +36 -0
  307. package/dist/util/memo/bounded-memo.js +58 -0
  308. package/docs/README.md +54 -0
  309. package/docs/ai-skill/README.md +69 -0
  310. package/docs/cli/README.md +284 -0
  311. package/docs/factories/README.md +140 -0
  312. package/docs/lint/README.md +176 -0
  313. package/docs/non-aws-dependencies/README.md +261 -0
  314. package/docs/sdk/README.md +274 -0
  315. package/docs/serve/README.md +1126 -0
  316. package/docs/services/acm/README.md +823 -0
  317. package/docs/services/apigateway/README.md +1919 -0
  318. package/docs/services/apigatewayv2/README.md +2678 -0
  319. package/docs/services/athena/README.md +1044 -0
  320. package/docs/services/bedrock/README.md +350 -0
  321. package/docs/services/cloudformation/README.md +3625 -0
  322. package/docs/services/cloudfront/README.md +3098 -0
  323. package/docs/services/cloudwatch/README.md +422 -0
  324. package/docs/services/cognito/README.md +4924 -0
  325. package/docs/services/dynamodb/README.md +3785 -0
  326. package/docs/services/ecr/README.md +302 -0
  327. package/docs/services/ecs/README.md +2021 -0
  328. package/docs/services/elbv2/README.md +1901 -0
  329. package/docs/services/eventbridge/README.md +1118 -0
  330. package/docs/services/firehose/README.md +811 -0
  331. package/docs/services/glue/README.md +507 -0
  332. package/docs/services/iam/README.md +1578 -0
  333. package/docs/services/kinesis/README.md +455 -0
  334. package/docs/services/kms/README.md +664 -0
  335. package/docs/services/lambda/README.md +3719 -0
  336. package/docs/services/logs/README.md +882 -0
  337. package/docs/services/organizations/README.md +574 -0
  338. package/docs/services/personalize/README.md +835 -0
  339. package/docs/services/rekognition/README.md +1070 -0
  340. package/docs/services/route53/README.md +1783 -0
  341. package/docs/services/s3/README.md +3029 -0
  342. package/docs/services/scheduler/README.md +814 -0
  343. package/docs/services/secretsmanager/README.md +670 -0
  344. package/docs/services/ses/README.md +1013 -0
  345. package/docs/services/sns/README.md +1904 -0
  346. package/docs/services/sqs/README.md +1185 -0
  347. package/docs/services/ssm/README.md +1096 -0
  348. package/docs/services/stepfunctions/README.md +1341 -0
  349. package/docs/services/sts/README.md +245 -0
  350. package/docs/services/wafv2/README.md +1380 -0
  351. package/docs/terraform/README.md +248 -0
  352. package/docs/time/README.md +284 -0
  353. package/llms.txt +59 -0
  354. package/package.json +7 -4
  355. package/dist/service/stepfunctions/command/machine/sim-state-machine-writes.d.ts +0 -25
@@ -0,0 +1,3098 @@
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`, or
1013
+ [CloudFormation bindings](https://yulinsim.dev/services/cloudformation/#cloudfront-function-bindings "CloudFront Function bindings usage docs")
1014
+ to back a template Resource with one. A handler reference is what to reach for first. It stops on a
1015
+ breakpoint and can close over the test's own state. Publish the source itself where the test is
1016
+ about the code the Distribution carries, down to the 10 KB it has to fit in. A
1017
+ [Lambda@Edge](#simulated-lambdaedge) function is an ordinary simulated Lambda function, and the same
1018
+ choice covers it.
1019
+
1020
+ The `host` header a function sees is the hostname the request was made to CloudFront with, being the
1021
+ Distribution domain name or one of its alternate domain names. Requests served on localhost arrive
1022
+ with a Yulin-local host such as `distro123.cloudfront.net.sim-aws.localhost:52341`, and the local
1023
+ suffix and port are dropped before the function runs. A function building a URL from
1024
+ `event.request.headers.host.value` behaves as it would on AWS. As on AWS, `host` is read-only, and a
1025
+ host a function writes is discarded before the Origin sees it.
1026
+
1027
+ A header arriving more than once reaches the Function as one entry holding every value it arrived
1028
+ with. `value` carries the first, and `multiValue` carries all of them, the same shape a repeated
1029
+ query string parameter has. A response setting three cookies gives a viewer-response Function this:
1030
+
1031
+ ```typescript
1032
+ event.response.headers["set-cookie"];
1033
+ // {
1034
+ // value: "session=abc123; Path=/",
1035
+ // multiValue: [
1036
+ // { value: "session=abc123; Path=/" },
1037
+ // { value: "state=; Max-Age=0" },
1038
+ // { value: "signed-in=1; Path=/" },
1039
+ // ],
1040
+ // }
1041
+ ```
1042
+
1043
+ A Function returning that response untouched leaves all three cookies on their way to the viewer. A
1044
+ Function writing `multiValue` sends one header per value in it, and CloudFront ignores `value` while
1045
+ both are there. Writing `value` on its own sends a single header.
1046
+
1047
+ A Function reads the query string as the viewer spelled it. `?q=%E5%AE%B6` arrives as
1048
+ `event.request.querystring.q.value === "%E5%AE%B6"`, `?q=a+b` keeps its plus, and a percent-encoded
1049
+ parameter name stays encoded. Whatever a Function leaves in `querystring` goes on to the Origin as
1050
+ it stands. A Function returning the request untouched forwards the query byte for byte, and one
1051
+ writing a value of its own encodes it (the same job it has on AWS).
1052
+
1053
+ ```typescript sim-cloudfront-function
1054
+ /**
1055
+ * Simulated CloudFront Functions.
1056
+ */
1057
+
1058
+ import {
1059
+ CreateDistributionCommand,
1060
+ CreateFunctionCommand,
1061
+ } from "@aws-sdk/client-cloudfront";
1062
+ import {
1063
+ CreateBucketCommand,
1064
+ PutBucketPolicyCommand,
1065
+ PutPublicAccessBlockCommand,
1066
+ } from "@aws-sdk/client-s3";
1067
+
1068
+ import { SimAws } from "@kensio/yulin";
1069
+ import {
1070
+ makeCffFunctionCodeInput,
1071
+ type CloudFrontFunction,
1072
+ } from "@kensio/yulin/cloudfront";
1073
+ import { serveSimAws } from "@kensio/yulin/serve";
1074
+
1075
+ const simAws = new SimAws();
1076
+ const srv = await serveSimAws({ simAws });
1077
+
1078
+ try {
1079
+ const simS3 = simAws.s3();
1080
+ const simCloudFront = simAws.cloudFront();
1081
+
1082
+ await simS3.createBucket(
1083
+ new CreateBucketCommand({
1084
+ Bucket: "foo-bucket",
1085
+ }),
1086
+ );
1087
+
1088
+ // A CloudFront S3 Origin with no origin access control reads the Bucket
1089
+ // anonymously, so what it serves has to be publicly readable.
1090
+ await simS3.putPublicAccessBlock(
1091
+ new PutPublicAccessBlockCommand({
1092
+ Bucket: "foo-bucket",
1093
+ PublicAccessBlockConfiguration: {
1094
+ BlockPublicAcls: true,
1095
+ IgnorePublicAcls: true,
1096
+ },
1097
+ }),
1098
+ );
1099
+ await simS3.putBucketPolicy(
1100
+ new PutBucketPolicyCommand({
1101
+ Bucket: "foo-bucket",
1102
+ Policy: JSON.stringify({
1103
+ Version: "2012-10-17",
1104
+ Statement: {
1105
+ Effect: "Allow",
1106
+ Principal: "*",
1107
+ Action: "s3:GetObject",
1108
+ Resource: "arn:aws:s3:::foo-bucket/*",
1109
+ },
1110
+ }),
1111
+ }),
1112
+ );
1113
+
1114
+ function viewerRequestFunction(
1115
+ event: CloudFrontFunction.ViewerRequestEvent,
1116
+ ): CloudFrontFunction.Request | CloudFrontFunction.Response {
1117
+ if (event.request.uri === "/old-page.html") {
1118
+ return {
1119
+ statusCode: 302,
1120
+ statusDescription: "Found",
1121
+ headers: {
1122
+ location: {
1123
+ value: "https://example.test/new-page.html",
1124
+ },
1125
+ },
1126
+ };
1127
+ }
1128
+
1129
+ return event.request;
1130
+ }
1131
+
1132
+ const functionCreation = await simCloudFront.createFunction(
1133
+ new CreateFunctionCommand({
1134
+ Name: "redirect-old-page",
1135
+ FunctionConfig: {
1136
+ Comment: "Redirect old page",
1137
+ Runtime: "cloudfront-js-2.0",
1138
+ },
1139
+ FunctionCode: makeCffFunctionCodeInput(viewerRequestFunction),
1140
+ }),
1141
+ );
1142
+
1143
+ const distributionCreation = await simCloudFront.createDistribution(
1144
+ new CreateDistributionCommand({
1145
+ DistributionConfig: {
1146
+ CallerReference: "function-cdn",
1147
+ Comment: "Function CDN",
1148
+ Enabled: true,
1149
+ Origins: {
1150
+ Quantity: 1,
1151
+ Items: [
1152
+ {
1153
+ Id: "assets-origin",
1154
+ DomainName: "foo-bucket.s3.amazonaws.com",
1155
+ S3OriginConfig: {
1156
+ OriginAccessIdentity: "",
1157
+ },
1158
+ },
1159
+ ],
1160
+ },
1161
+ DefaultCacheBehavior: {
1162
+ TargetOriginId: "assets-origin",
1163
+ ViewerProtocolPolicy: "allow-all",
1164
+ FunctionAssociations: {
1165
+ Quantity: 1,
1166
+ Items: [
1167
+ {
1168
+ EventType: "viewer-request",
1169
+ FunctionARN: functionCreation.FunctionMetadata.FunctionARN,
1170
+ },
1171
+ ],
1172
+ },
1173
+ },
1174
+ },
1175
+ }),
1176
+ );
1177
+
1178
+ const distroHostname = distributionCreation.Distribution!.DomainName!;
1179
+
1180
+ const url = srv.localUrl(`http://${distroHostname}/old-page.html`);
1181
+ const response = await fetch(url, { redirect: "manual" });
1182
+
1183
+ console.log(response.status);
1184
+ console.log(response.headers.get("location"));
1185
+ } finally {
1186
+ await srv.close();
1187
+ }
1188
+ ```
1189
+
1190
+ If your CloudFront Function code lives in a module that exports the handler, use
1191
+ `cloudFrontFunctionSourceFromModule` in your CDK Stack to load it as inline CloudFront Function
1192
+ code. This lets the same function file use an export like `export function handler(...)` while
1193
+ still being accepted by CloudFront Function inline code.
1194
+
1195
+ ```typescript sim-cloudfront-function-module-export
1196
+ /**
1197
+ * cloudFrontFunctionSourceFromModule util function
1198
+ */
1199
+
1200
+ import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
1201
+ import { Stack } from "aws-cdk-lib";
1202
+ import type { Construct } from "constructs";
1203
+
1204
+ import { cloudFrontFunctionSourceFromModule } from "@kensio/yulin/cloudfront";
1205
+
1206
+ /**
1207
+ * Example CDK stack using cloudFrontFunctionSourceFromModule to extract source
1208
+ * code for a CloudFront Function handler from a module that uses `export`.
1209
+ */
1210
+ export class WebsiteStack extends Stack {
1211
+ constructor(scope: Construct, id: string) {
1212
+ super(scope, id);
1213
+
1214
+ new cloudfront.Function(this, "RewriteFunction", {
1215
+ code: cloudfront.FunctionCode.fromInline(
1216
+ cloudFrontFunctionSourceFromModule("src/cff/rewrite.cff.js"),
1217
+ ),
1218
+ runtime: cloudfront.FunctionRuntime.JS_2_0,
1219
+ });
1220
+ }
1221
+ }
1222
+ ```
1223
+
1224
+ The referenced CloudFront Function module can then keep an exported handler:
1225
+
1226
+ ```javascript
1227
+ /**
1228
+ * @typedef {import("@kensio/yulin/cloudfront").CloudFrontFunction.Event} CloudFrontEvent
1229
+ * @typedef {import("@kensio/yulin/cloudfront").CloudFrontFunction.Request} CloudFrontRequest
1230
+ * @typedef {import("@kensio/yulin/cloudfront").CloudFrontFunction.Response} CloudFrontResponse
1231
+ */
1232
+
1233
+ /**
1234
+ * Handles a CloudFront Functions viewer request event.
1235
+ * @param {CloudFrontEvent} event - The CloudFront Functions event object.
1236
+ * @returns {CloudFrontRequest|CloudFrontResponse} A CloudFront request object or response object.
1237
+ */
1238
+ export function handler(event) {
1239
+ var request = event.request;
1240
+ var uri = request.uri;
1241
+
1242
+ if (uri.endsWith("/")) {
1243
+ request.uri += "index.html";
1244
+ } else if (!uri.includes(".") && !uri.endsWith("/")) {
1245
+ request.uri += "/index.html";
1246
+ }
1247
+
1248
+ return request;
1249
+ }
1250
+ ```
1251
+
1252
+ CloudFront Functions run JS2, ECMAScript 5.1 plus a named subset of ES 6 to 12. It refuses
1253
+ constructs ordinary JavaScript allows. Yulin publishes ESLint and Oxlint configs that report those
1254
+ refusals in the editor, ahead of publication. See
1255
+ [Linting CloudFront Functions JS2](https://yulinsim.dev/lint/ "CloudFront Functions JS2 lint config usage docs").
1256
+
1257
+ CloudFront also caps Function code at 10 KB, counted on the source as uploaded, comments and all.
1258
+ Simulated `CreateFunction` refuses anything larger with `FunctionSizeLimitExceeded`, as the real
1259
+ service does. A test that deploys the Stack reports the overrun where the rest of the suite runs,
1260
+ ahead of `cdk deploy`. A handler passed as a function reference carries no source to count, and the
1261
+ limit leaves it alone.
1262
+
1263
+ ### Reading a Function back
1264
+
1265
+ `ListFunctions` reports the Functions the Account holds. Each carries the `FunctionConfig` it was
1266
+ created with and the `FunctionMetadata` CloudFront gave it. `DescribeFunction` reports one by name,
1267
+ and `GetFunction` reports its code. A test that wants to know which Function a stack deployed, and
1268
+ on which runtime, asks one of these.
1269
+
1270
+ ```typescript sim-cloudfront-function-read
1271
+ /**
1272
+ * Reading a deployed CloudFront Function back.
1273
+ */
1274
+
1275
+ import {
1276
+ CreateFunctionCommand,
1277
+ DescribeFunctionCommand,
1278
+ GetFunctionCommand,
1279
+ ListFunctionsCommand,
1280
+ } from "@aws-sdk/client-cloudfront";
1281
+
1282
+ import { SimAws } from "@kensio/yulin";
1283
+
1284
+ const simAws = new SimAws();
1285
+ const simCloudFront = simAws.cloudFront();
1286
+
1287
+ await simCloudFront.createFunction(
1288
+ new CreateFunctionCommand({
1289
+ Name: "beacon",
1290
+ FunctionConfig: {
1291
+ Comment: "Answers the analytics beacon",
1292
+ Runtime: "cloudfront-js-2.0",
1293
+ },
1294
+ FunctionCode: Buffer.from(`
1295
+ function handler(event) {
1296
+ return { statusCode: 204, statusDescription: "No Content" };
1297
+ }
1298
+ `),
1299
+ }),
1300
+ );
1301
+
1302
+ // CloudFront publishes a new Function in the background.
1303
+ await simAws.backgroundTasksComplete();
1304
+
1305
+ const listed = await simCloudFront.listFunctions(new ListFunctionsCommand({}));
1306
+
1307
+ // cloudfront-js-2.0
1308
+ console.log(listed.FunctionList.Items[0]?.FunctionConfig.Runtime);
1309
+
1310
+ const described = await simCloudFront.describeFunction(
1311
+ new DescribeFunctionCommand({ Name: "beacon", Stage: "LIVE" }),
1312
+ );
1313
+
1314
+ // Answers the analytics beacon
1315
+ console.log(described.FunctionSummary.FunctionConfig.Comment);
1316
+
1317
+ const got = await simCloudFront.getFunction(
1318
+ new GetFunctionCommand({ Name: "beacon" }),
1319
+ );
1320
+
1321
+ // The source the Function was created with.
1322
+ console.log(Buffer.from(got.FunctionCode).toString());
1323
+ ```
1324
+
1325
+ `Stage` picks the copy to read. A Function is created into `DEVELOPMENT` and reaches `LIVE` once
1326
+ CloudFront has published it, and an omitted `Stage` means `DEVELOPMENT`, as it does on AWS. Asking
1327
+ for the `LIVE` copy of a Function still waiting to publish fails with `NoSuchFunctionExists`, and so
1328
+ does a name the Account holds no Function under.
1329
+
1330
+ `CreatedTime` and `LastModifiedTime` come off the simulated clock. Freezing time before the deploy
1331
+ pins both to an instant the test picked (see
1332
+ [Controlling simulated time](https://yulinsim.dev/time/ "Simulated time usage docs")).
1333
+
1334
+ A Function backed by a handler function reference was given no source to keep. `GetFunction` answers
1335
+ with that handler's own source text. That is the code the Function runs.
1336
+
1337
+ The whole list comes back. `Marker` and `MaxItems` paging is left out, matching the paging left out
1338
+ of the other simulated listings. `UpdateFunction`, `PublishFunction` and `TestFunction` are not
1339
+ simulated.
1340
+
1341
+ ### Calling a Function handler without a Distribution
1342
+
1343
+ A test of the handler on its own, with no Distribution in front of it, still has to pass it a whole
1344
+ event. `cloudFrontViewerRequestEventFactory` and `cloudFrontViewerResponseEventFactory` make the
1345
+ two, so such a test says what the request or the response was and leaves the rest alone:
1346
+
1347
+ ```typescript sim-cloudfront-function-event-factory
1348
+ /**
1349
+ * Making a CloudFront Functions event to call a handler with.
1350
+ */
1351
+
1352
+ import { VariantFactory } from "@kensio/part-factory";
1353
+
1354
+ import {
1355
+ cloudFrontViewerResponseEventFactory,
1356
+ type CloudFrontFunction,
1357
+ } from "@kensio/yulin/cloudfront";
1358
+
1359
+ function securityHeadersHandler(
1360
+ event: CloudFrontFunction.ViewerResponseEvent,
1361
+ ): CloudFrontFunction.Response {
1362
+ const response = event.response;
1363
+ const contentType = response.headers["content-type"]?.value ?? "";
1364
+
1365
+ if (contentType.startsWith("text/html")) {
1366
+ response.headers["x-frame-options"] = { value: "DENY" };
1367
+ }
1368
+
1369
+ return response;
1370
+ }
1371
+
1372
+ // A response carrying a page. Those are the ones the policy is about.
1373
+ const documentResponseFactory = new VariantFactory(
1374
+ cloudFrontViewerResponseEventFactory,
1375
+ {
1376
+ response: {
1377
+ headers: { "content-type": { value: "text/html; charset=utf-8" } },
1378
+ },
1379
+ },
1380
+ );
1381
+
1382
+ const page = securityHeadersHandler(documentResponseFactory.make());
1383
+
1384
+ // DENY
1385
+ console.log(page.headers["x-frame-options"]?.value);
1386
+
1387
+ // One response, for a test about a single asset. Everything else about it, down
1388
+ // to the request that asked for it, is filled in as a served response's is.
1389
+ const asset = securityHeadersHandler(
1390
+ cloudFrontViewerResponseEventFactory.make({
1391
+ response: { headers: { "content-type": { value: "text/css" } } },
1392
+ }),
1393
+ );
1394
+
1395
+ // undefined
1396
+ console.log(asset.headers["x-frame-options"]?.value);
1397
+ ```
1398
+
1399
+ The defaults describe a request for `/cloudfront/` reaching the Distribution, with a `host` of
1400
+ `yulin.test`, a session cookie and a viewer address. A viewer-response event carries the request
1401
+ that asked for it as well as the response, and the response's own defaults are a status code and no
1402
+ headers.
1403
+
1404
+ The [event factories page](https://yulinsim.dev/factories/ "Test factories for AWS event shapes usage docs")
1405
+ covers what the factories have in common.
1406
+
1407
+ ## Simulated Lambda@Edge
1408
+
1409
+ A cache Behavior can run a Lambda function at any of CloudFront's four events through
1410
+ `LambdaFunctionAssociations`. Those are `viewer-request` and `viewer-response` at the edge, and
1411
+ `origin-request` and `origin-response` either side of the Origin fetch. Where a CloudFront Function
1412
+ is a small piece of JavaScript running in CloudFront's own runtime, a Lambda@Edge function is an
1413
+ ordinary simulated Lambda function, with an execution role, an environment and whatever SDK calls
1414
+ its handler makes.
1415
+
1416
+ Three things about Lambda@Edge catch people out on AWS, and simulated CloudFront refuses all three
1417
+ the way AWS refuses them, when the Distribution is written rather than when a request arrives.
1418
+
1419
+ - **The function lives in `us-east-1`**, wherever the rest of the stack lives.
1420
+ - **The association names a published version**, such as `:1`. An unqualified ARN, `$LATEST` and an
1421
+ alias are each refused.
1422
+ - **The execution role trusts `edgelambda.amazonaws.com`** as well as `lambda.amazonaws.com`. A role
1423
+ set up for an ordinary function is the usual reason a first Lambda@Edge deploy fails.
1424
+
1425
+ ```typescript sim-cloudfront-lambda-edge
1426
+ /**
1427
+ * A Lambda@Edge function rewriting a request at the viewer.
1428
+ */
1429
+
1430
+ import { CreateDistributionCommand } from "@aws-sdk/client-cloudfront";
1431
+ import { CreateRoleCommand } from "@aws-sdk/client-iam";
1432
+ import {
1433
+ CreateFunctionCommand,
1434
+ PublishVersionCommand,
1435
+ } from "@aws-sdk/client-lambda";
1436
+
1437
+ import { SimAws } from "@kensio/yulin";
1438
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
1439
+ import type { LambdaAtEdge } from "@kensio/yulin/cloudfront";
1440
+
1441
+ const simAws = new SimAws();
1442
+
1443
+ // A Lambda@Edge execution role trusts both service principals.
1444
+ const role = await simAws.iam().createRole(
1445
+ new CreateRoleCommand({
1446
+ RoleName: "EdgeRewriteRole",
1447
+ AssumeRolePolicyDocument: JSON.stringify({
1448
+ Version: "2012-10-17",
1449
+ Statement: {
1450
+ Effect: "Allow",
1451
+ Principal: {
1452
+ Service: ["lambda.amazonaws.com", "edgelambda.amazonaws.com"],
1453
+ },
1454
+ Action: "sts:AssumeRole",
1455
+ },
1456
+ }),
1457
+ }),
1458
+ );
1459
+
1460
+ // The function has to be in us-east-1, and the Behavior names a version.
1461
+ const edgeLambda = simAws.region("us-east-1").lambda();
1462
+
1463
+ await edgeLambda.createFunction(
1464
+ new CreateFunctionCommand({
1465
+ FunctionName: "rewrite-uri",
1466
+ Role: role.Role.Arn,
1467
+ Code: {
1468
+ ZipFile: makeLambdaZipFileInput((event: LambdaAtEdge.RequestEvent) => {
1469
+ const { request } = event.Records[0].cf;
1470
+
1471
+ // A header is a list keyed by its lowercase name, and a status is a
1472
+ // string. Both differ from the CloudFront Functions shapes.
1473
+ if (request.headers["x-preview"]?.[0]?.value === "1") {
1474
+ return {
1475
+ status: "302",
1476
+ headers: {
1477
+ location: [{ key: "Location", value: "/preview.html" }],
1478
+ },
1479
+ };
1480
+ }
1481
+
1482
+ request.uri = "/index.html";
1483
+
1484
+ return request;
1485
+ }),
1486
+ },
1487
+ }),
1488
+ );
1489
+
1490
+ const version = await edgeLambda.publishVersion(
1491
+ new PublishVersionCommand({ FunctionName: "rewrite-uri" }),
1492
+ );
1493
+
1494
+ await simAws.cloudFront().createDistribution(
1495
+ new CreateDistributionCommand({
1496
+ DistributionConfig: {
1497
+ CallerReference: "edge-rewrite",
1498
+ Comment: "Rewriting at the viewer",
1499
+ Enabled: true,
1500
+ Origins: {
1501
+ Quantity: 1,
1502
+ Items: [
1503
+ {
1504
+ Id: "site-origin",
1505
+ DomainName: "edge-site.s3.amazonaws.com",
1506
+ S3OriginConfig: { OriginAccessIdentity: "" },
1507
+ },
1508
+ ],
1509
+ },
1510
+ DefaultCacheBehavior: {
1511
+ TargetOriginId: "site-origin",
1512
+ ViewerProtocolPolicy: "allow-all",
1513
+ LambdaFunctionAssociations: {
1514
+ Quantity: 1,
1515
+ Items: [
1516
+ {
1517
+ EventType: "viewer-request",
1518
+ LambdaFunctionARN: version.FunctionArn,
1519
+ IncludeBody: false,
1520
+ },
1521
+ ],
1522
+ },
1523
+ },
1524
+ },
1525
+ }),
1526
+ );
1527
+ ```
1528
+
1529
+ A `viewer-request` handler returning the request carries on to the Origin with whatever it changed.
1530
+ Returning a response answers the viewer there and then, and the Origin is never read. A
1531
+ `viewer-response` handler returns the response the viewer gets.
1532
+
1533
+ Set `IncludeBody` to give a `viewer-request` or `origin-request` handler the request body, which
1534
+ arrives base64 encoded under `request.body.data`. A handler setting `request.body.action` to
1535
+ `replace` sends its own body to the Origin. The field belongs to the two request events, and an
1536
+ association setting it on `viewer-response` or `origin-response` is refused, as CloudFront refuses
1537
+ one.
1538
+
1539
+ A handler that throws answers the viewer with a 502, as CloudFront answers a failed edge function.
1540
+ The error reaches the function's own output and nothing else.
1541
+
1542
+ ### The origin events
1543
+
1544
+ An `origin-request` function runs after the Behavior has resolved the Origin and before the fetch.
1545
+ Its event carries `request.origin`, holding the Origin the fetch is about to read, under `custom` or
1546
+ `s3` for the kind it is. A handler rewriting `origin.custom.domainName` sends the fetch to another
1547
+ Origin, and one rewriting `path` reads under another prefix. A header added to `customHeaders`
1548
+ reaches the Origin and the viewer never sees it. A handler returning a response answers the viewer
1549
+ with the Origin unread.
1550
+
1551
+ An `origin-response` function runs after the fetch and before the custom error page replaces an
1552
+ error status. It runs on whatever the Origin answered, including a 400 and above. That is where the
1553
+ origin events differ from the viewer events, and CloudFront documents it. Returning a response
1554
+ replaces what the viewer gets.
1555
+
1556
+ At both origin events the `host` header holds the Origin's own domain name. A viewer event shows the
1557
+ domain the viewer used.
1558
+
1559
+ ```typescript sim-cloudfront-lambda-edge-origin
1560
+ /**
1561
+ * A Lambda@Edge function choosing the Origin, and another one stamping what
1562
+ * that Origin answered.
1563
+ */
1564
+
1565
+ import { CreateDistributionCommand } from "@aws-sdk/client-cloudfront";
1566
+ import { CreateRoleCommand } from "@aws-sdk/client-iam";
1567
+ import {
1568
+ CreateFunctionCommand,
1569
+ PublishVersionCommand,
1570
+ } from "@aws-sdk/client-lambda";
1571
+
1572
+ import { SimAws } from "@kensio/yulin";
1573
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
1574
+ import type { LambdaAtEdge } from "@kensio/yulin/cloudfront";
1575
+
1576
+ const simAws = new SimAws();
1577
+
1578
+ // A Lambda@Edge execution role trusts both service principals, at the origin
1579
+ // events as at the viewer events.
1580
+ const role = await simAws.iam().createRole(
1581
+ new CreateRoleCommand({
1582
+ RoleName: "EdgeOriginRole",
1583
+ AssumeRolePolicyDocument: JSON.stringify({
1584
+ Version: "2012-10-17",
1585
+ Statement: {
1586
+ Effect: "Allow",
1587
+ Principal: {
1588
+ Service: ["lambda.amazonaws.com", "edgelambda.amazonaws.com"],
1589
+ },
1590
+ Action: "sts:AssumeRole",
1591
+ },
1592
+ }),
1593
+ }),
1594
+ );
1595
+
1596
+ const edgeLambda = simAws.region("us-east-1").lambda();
1597
+
1598
+ await edgeLambda.createFunction(
1599
+ new CreateFunctionCommand({
1600
+ FunctionName: "route-origin",
1601
+ Role: role.Role.Arn,
1602
+ Code: {
1603
+ ZipFile: makeLambdaZipFileInput(
1604
+ (event: LambdaAtEdge.OriginRequestEvent) => {
1605
+ const { request } = event.Records[0].cf;
1606
+ const { custom } = request.origin;
1607
+
1608
+ if (custom === undefined) {
1609
+ return request;
1610
+ }
1611
+
1612
+ // Everything under /api is served by the second Origin.
1613
+ if (request.uri.startsWith("/api/")) {
1614
+ custom.domainName = "orders.example.test";
1615
+ }
1616
+
1617
+ // A header the viewer never sent and never sees.
1618
+ custom.customHeaders["x-from-cloudfront"] = [
1619
+ { key: "X-From-CloudFront", value: "yes" },
1620
+ ];
1621
+
1622
+ return request;
1623
+ },
1624
+ ),
1625
+ },
1626
+ }),
1627
+ );
1628
+
1629
+ const routeVersion = await edgeLambda.publishVersion(
1630
+ new PublishVersionCommand({ FunctionName: "route-origin" }),
1631
+ );
1632
+
1633
+ await edgeLambda.createFunction(
1634
+ new CreateFunctionCommand({
1635
+ FunctionName: "stamp-origin-response",
1636
+ Role: role.Role.Arn,
1637
+ Code: {
1638
+ ZipFile: makeLambdaZipFileInput(
1639
+ (event: LambdaAtEdge.OriginResponseEvent): LambdaAtEdge.Response => {
1640
+ const { response } = event.Records[0].cf;
1641
+
1642
+ // This runs for an Origin error too, so the status is worth keeping.
1643
+ return {
1644
+ ...response,
1645
+ headers: {
1646
+ ...response.headers,
1647
+ "x-origin-status": [
1648
+ { key: "X-Origin-Status", value: response.status },
1649
+ ],
1650
+ },
1651
+ };
1652
+ },
1653
+ ),
1654
+ },
1655
+ }),
1656
+ );
1657
+
1658
+ const stampVersion = await edgeLambda.publishVersion(
1659
+ new PublishVersionCommand({ FunctionName: "stamp-origin-response" }),
1660
+ );
1661
+
1662
+ const customOriginConfig = {
1663
+ HTTPPort: 80,
1664
+ HTTPSPort: 443,
1665
+ OriginProtocolPolicy: "https-only",
1666
+ } as const;
1667
+
1668
+ await simAws.cloudFront().createDistribution(
1669
+ new CreateDistributionCommand({
1670
+ DistributionConfig: {
1671
+ CallerReference: "edge-origin-routing",
1672
+ Comment: "Choosing the Origin at the edge",
1673
+ Enabled: true,
1674
+ Origins: {
1675
+ Quantity: 2,
1676
+ Items: [
1677
+ {
1678
+ Id: "site-origin",
1679
+ DomainName: "site.example.test",
1680
+ CustomOriginConfig: customOriginConfig,
1681
+ },
1682
+ {
1683
+ Id: "orders-origin",
1684
+ DomainName: "orders.example.test",
1685
+ CustomOriginConfig: customOriginConfig,
1686
+ },
1687
+ ],
1688
+ },
1689
+ DefaultCacheBehavior: {
1690
+ TargetOriginId: "site-origin",
1691
+ ViewerProtocolPolicy: "allow-all",
1692
+ LambdaFunctionAssociations: {
1693
+ Quantity: 2,
1694
+ Items: [
1695
+ {
1696
+ EventType: "origin-request",
1697
+ LambdaFunctionARN: routeVersion.FunctionArn,
1698
+ },
1699
+ {
1700
+ EventType: "origin-response",
1701
+ LambdaFunctionARN: stampVersion.FunctionArn,
1702
+ },
1703
+ ],
1704
+ },
1705
+ },
1706
+ },
1707
+ }),
1708
+ );
1709
+ ```
1710
+
1711
+ Two Origin rewrites are refused, and the viewer gets the 502 a failed edge function gets, carrying
1712
+ the reason (see [Limitations](#limitations)). One switches an Origin between `custom` and `s3`. The
1713
+ other moves an S3 Origin to another Bucket.
1714
+
1715
+ ### Which edge function runs where
1716
+
1717
+ CloudFront takes one edge function per event type, and it does not combine CloudFront Functions with
1718
+ Lambda@Edge at the viewer events. A Behavior with a viewer-request CloudFront Function and a
1719
+ viewer-response Lambda@Edge function is refused, and so is a Behavior naming both at one event type.
1720
+ Simulated CloudFront refuses the same combinations. The rule stops at the viewer. A viewer-request
1721
+ CloudFront Function runs alongside a Lambda@Edge function on either origin event.
1722
+
1723
+ Neither kind runs at the viewer response once the Origin has answered 400 or higher. CloudFront
1724
+ skips that event for an Origin error, and the status the Origin returned is what decides it (see
1725
+ [Limitations](#limitations)).
1726
+
1727
+ Both kinds of function see the `host` header as the hostname the viewer reached CloudFront with,
1728
+ rather than the Yulin-local host a request served on localhost arrives with. As on AWS, `host` is
1729
+ read-only at the viewer request, and a host a handler writes is discarded before the Origin sees it.
1730
+
1731
+ ### From CloudFormation
1732
+
1733
+ `AWS::CloudFront::Distribution` takes `LambdaFunctionAssociations` on `DefaultCacheBehavior` and on
1734
+ any entry of `CacheBehaviors`. CloudFormation writes the list as a plain array where the SDK writes
1735
+ the `Quantity` and `Items` pair. `Ref` on an `AWS::Lambda::Version` answers the qualified function
1736
+ ARN. That is the value an association names, and the two fit together directly:
1737
+
1738
+ ```yaml
1739
+ EdgeVersion:
1740
+ Type: AWS::Lambda::Version
1741
+ Properties:
1742
+ FunctionName: !Ref RewriteFunction
1743
+
1744
+ SiteDistribution:
1745
+ Type: AWS::CloudFront::Distribution
1746
+ Properties:
1747
+ DistributionConfig:
1748
+ DefaultCacheBehavior:
1749
+ TargetOriginId: SiteOrigin
1750
+ ViewerProtocolPolicy: allow-all
1751
+ LambdaFunctionAssociations:
1752
+ - EventType: viewer-request
1753
+ LambdaFunctionARN: !Ref EdgeVersion
1754
+ ```
1755
+
1756
+ The function still has to live in us-east-1. A stack holding one is a us-east-1 stack.
1757
+
1758
+ An association naming a function version this simulation does not hold is left out of the deployed
1759
+ Distribution and recorded on `stack.ignoredProperties`, under the event type it was on. A template
1760
+ pointing at a function in a real account is the usual reason. The rest of the Behavior deploys, and
1761
+ the event the skipped association was on is left empty. A test that cares reads the record.
1762
+
1763
+ Everything real CloudFront refuses still fails the deployment. A function outside us-east-1, an ARN
1764
+ without a version qualifier, an execution role missing the `edgelambda.amazonaws.com` trust, an
1765
+ `EventType` that is none of CloudFront's four, two functions on one event type and a viewer event
1766
+ running both kinds of edge function each fail a real deploy of the same template.
1767
+
1768
+ CDK reaches a Behavior through `edgeLambdas`, given a `lambda.Version` from the same stack:
1769
+
1770
+ ```typescript sim-cloudfront-lambda-edge-cdk
1771
+ /**
1772
+ * A CDK Distribution running a Lambda@Edge function at the viewer request.
1773
+ */
1774
+
1775
+ import { Stack } from "aws-cdk-lib";
1776
+ import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
1777
+ import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
1778
+ import * as iam from "aws-cdk-lib/aws-iam";
1779
+ import * as lambda from "aws-cdk-lib/aws-lambda";
1780
+ import * as s3 from "aws-cdk-lib/aws-s3";
1781
+ import type { Construct } from "constructs";
1782
+
1783
+ /**
1784
+ * Example CDK stack whose Distribution rewrites every request at the edge.
1785
+ *
1786
+ * The stack is in us-east-1, the one Region CloudFront runs a Lambda@Edge
1787
+ * function from.
1788
+ */
1789
+ export class SiteStack extends Stack {
1790
+ constructor(scope: Construct, id: string) {
1791
+ super(scope, id, { env: { region: "us-east-1" } });
1792
+
1793
+ const siteBucket = new s3.Bucket(this, "SiteBucket");
1794
+
1795
+ // A Lambda@Edge execution role trusts both service principals.
1796
+ const edgeRole = new iam.Role(this, "EdgeRole", {
1797
+ assumedBy: new iam.CompositePrincipal(
1798
+ new iam.ServicePrincipal("lambda.amazonaws.com"),
1799
+ new iam.ServicePrincipal("edgelambda.amazonaws.com"),
1800
+ ),
1801
+ });
1802
+
1803
+ const rewriteFunction = new lambda.Function(this, "RewriteFunction", {
1804
+ runtime: lambda.Runtime.NODEJS_22_X,
1805
+ handler: "index.handler",
1806
+ role: edgeRole,
1807
+ code: lambda.Code.fromInline(`
1808
+ exports.handler = async (event) => {
1809
+ const { request } = event.Records[0].cf;
1810
+ request.uri = "/index.html";
1811
+ return request;
1812
+ };
1813
+ `),
1814
+ });
1815
+
1816
+ new cloudfront.Distribution(this, "SiteDistribution", {
1817
+ defaultBehavior: {
1818
+ origin: origins.S3BucketOrigin.withOriginAccessControl(siteBucket),
1819
+ edgeLambdas: [
1820
+ {
1821
+ // edgeLambdas takes a published version, and currentVersion
1822
+ // is one.
1823
+ functionVersion: rewriteFunction.currentVersion,
1824
+ eventType: cloudfront.LambdaEdgeEventType.VIEWER_REQUEST,
1825
+ },
1826
+ ],
1827
+ },
1828
+ });
1829
+ }
1830
+ }
1831
+ ```
1832
+
1833
+ `cloudfront.experimental.EdgeFunction` deploys from a us-east-1 stack, where the construct creates
1834
+ the function alongside everything else.
1835
+
1836
+ From a stack in any other Region the construct writes the function, its published version and an SSM
1837
+ parameter holding the version ARN into a support stack in us-east-1. The stack using the function
1838
+ reads that parameter back through a `Custom::CrossRegionStringParameterReader` resource, and the
1839
+ Behavior's `LambdaFunctionARN` is an `Fn::GetAtt` on it. Simulated CloudFormation makes that read
1840
+ itself, against the Region the resource names, and the association ends up holding the ARN the
1841
+ support stack published. Both stacks have to deploy, which is what deploying the whole cloud
1842
+ assembly does:
1843
+
1844
+ ```typescript
1845
+ await simAws.cloudFormation().deployCdkOut("cdk.out");
1846
+ ```
1847
+
1848
+ Deploy the using stack's template on its own and no parameter has been written. The read finds
1849
+ nothing and says so on `stack.ignoredProperties`, the Behavior deploys without the association, and
1850
+ the site serves from the Origin (see [Limitations](#limitations)).
1851
+
1852
+ ## Web ACLs
1853
+
1854
+ A Distribution can put a WAFv2 web ACL in front of everything it serves. Name the web ACL's ARN in
1855
+ `WebACLId` and the Distribution evaluates it against every request that arrives. A request the web
1856
+ ACL blocks gets 403 from the edge. A request it allows carries on to the cache Behavior and the
1857
+ Origin.
1858
+
1859
+ CloudFront takes its web ACL this way. WAFv2's `AssociateWebACL` covers the regional resource types.
1860
+
1861
+ The web ACL has to be a `CLOUDFRONT` scope one, created in `us-east-1` (see
1862
+ [scopes](https://yulinsim.dev/services/wafv2/#scopes)). A `WebACLId` naming a `REGIONAL` web ACL, or one this
1863
+ simulation never created, is refused with `InvalidWebACLId` at `CreateDistribution` and at
1864
+ `UpdateDistribution`.
1865
+
1866
+ The web ACL decides before any other stage sees the request. A blocked request never reaches a
1867
+ viewer-request CloudFront Function, a cache Behavior, a response headers policy or the Origin.
1868
+
1869
+ A CloudFormation Distribution naming a web ACL this simulation does not hold deploys without one.
1870
+ The `WebACLId` lands on `stack.ignoredProperties` and every request is served, including the ones
1871
+ the web ACL would have decided. A template naming a web ACL from a real account is ordinary, and a
1872
+ site that failed to deploy over its firewall would cost a local dev server and a test suite every
1873
+ request they make. `CreateDistribution` still refuses the same `WebACLId`, as real CloudFront
1874
+ refuses it.
1875
+
1876
+ ```typescript sim-cloudfront-web-acl
1877
+ /**
1878
+ * Blocking a request to a Distribution with a web ACL.
1879
+ */
1880
+
1881
+ import { CreateDistributionCommand } from "@aws-sdk/client-cloudfront";
1882
+ import {
1883
+ CreateBucketCommand,
1884
+ PutBucketPolicyCommand,
1885
+ PutObjectCommand,
1886
+ } from "@aws-sdk/client-s3";
1887
+ import { CreateWebACLCommand } from "@aws-sdk/client-wafv2";
1888
+
1889
+ import { SimAws } from "@kensio/yulin";
1890
+ import { serveSimAws } from "@kensio/yulin/serve";
1891
+
1892
+ const simAws = new SimAws();
1893
+ const srv = await serveSimAws({ simAws });
1894
+
1895
+ try {
1896
+ const simS3 = simAws.s3();
1897
+ await simS3.createBucket(new CreateBucketCommand({ Bucket: "site-bucket" }));
1898
+ await simS3.putBucketPolicy(
1899
+ new PutBucketPolicyCommand({
1900
+ Bucket: "site-bucket",
1901
+ Policy: JSON.stringify({
1902
+ Version: "2012-10-17",
1903
+ Statement: {
1904
+ Effect: "Allow",
1905
+ Principal: "*",
1906
+ Action: "s3:GetObject",
1907
+ Resource: "arn:aws:s3:::site-bucket/*",
1908
+ },
1909
+ }),
1910
+ }),
1911
+ );
1912
+ await simS3.putObject(
1913
+ new PutObjectCommand({
1914
+ Bucket: "site-bucket",
1915
+ Key: "admin/users.html",
1916
+ ContentType: "text/html",
1917
+ Body: "<h1>Users</h1>",
1918
+ }),
1919
+ );
1920
+
1921
+ // A CLOUDFRONT scope web ACL lives in us-east-1, wherever the Distribution
1922
+ // was created from.
1923
+ const acl = await simAws
1924
+ .accountRegionScope(simAws.defaultAccountId, "us-east-1")
1925
+ .wafV2()
1926
+ .createWebAcl(
1927
+ new CreateWebACLCommand({
1928
+ Name: "site-acl",
1929
+ Scope: "CLOUDFRONT",
1930
+ DefaultAction: { Allow: {} },
1931
+ VisibilityConfig: {
1932
+ SampledRequestsEnabled: false,
1933
+ CloudWatchMetricsEnabled: false,
1934
+ MetricName: "site",
1935
+ },
1936
+ Rules: [
1937
+ {
1938
+ Name: "block-admin",
1939
+ Priority: 0,
1940
+ Action: { Block: {} },
1941
+ Statement: {
1942
+ ByteMatchStatement: {
1943
+ FieldToMatch: { UriPath: {} },
1944
+ PositionalConstraint: "STARTS_WITH",
1945
+ SearchString: Buffer.from("/admin"),
1946
+ TextTransformations: [{ Priority: 0, Type: "LOWERCASE" }],
1947
+ },
1948
+ },
1949
+ VisibilityConfig: {
1950
+ SampledRequestsEnabled: false,
1951
+ CloudWatchMetricsEnabled: false,
1952
+ MetricName: "block-admin",
1953
+ },
1954
+ },
1955
+ ],
1956
+ }),
1957
+ );
1958
+
1959
+ const creation = await simAws.cloudFront().createDistribution(
1960
+ new CreateDistributionCommand({
1961
+ DistributionConfig: {
1962
+ CallerReference: "guarded-site",
1963
+ Comment: "Site behind a web ACL",
1964
+ Enabled: true,
1965
+ WebACLId: acl.Summary!.ARN,
1966
+ Origins: {
1967
+ Quantity: 1,
1968
+ Items: [
1969
+ {
1970
+ Id: "site-origin",
1971
+ DomainName: "site-bucket.s3.amazonaws.com",
1972
+ S3OriginConfig: { OriginAccessIdentity: "" },
1973
+ },
1974
+ ],
1975
+ },
1976
+ DefaultCacheBehavior: {
1977
+ TargetOriginId: "site-origin",
1978
+ ViewerProtocolPolicy: "allow-all",
1979
+ },
1980
+ },
1981
+ }),
1982
+ );
1983
+
1984
+ const distroHostname = creation.Distribution!.DomainName!;
1985
+
1986
+ const blocked = await fetch(
1987
+ srv.localUrl(`http://${distroHostname}/admin/users.html`),
1988
+ );
1989
+ console.log(blocked.status); // 403
1990
+
1991
+ // The Bucket still holds the page. The request never got as far as the
1992
+ // Origin to ask for it.
1993
+ } finally {
1994
+ await srv.close();
1995
+ }
1996
+ ```
1997
+
1998
+ See [simulated WAFv2](https://yulinsim.dev/services/wafv2/) for what a rule can inspect and how a blocked request is
1999
+ answered.
2000
+
2001
+ ## Response headers policies
2002
+
2003
+ A response headers policy sets headers on everything a cache Behavior serves. Declare one as
2004
+ `AWS::CloudFront::ResponseHeadersPolicy` and point a Behavior's `ResponseHeadersPolicyId` at it with
2005
+ a `Ref`, which is what CDK's `ResponseHeadersPolicy` construct synthesizes.
2006
+
2007
+ ```typescript sim-cloudfront-response-headers-policy
2008
+ /**
2009
+ * Setting response headers on what a cache Behavior serves.
2010
+ */
2011
+
2012
+ import { PutObjectCommand } from "@aws-sdk/client-s3";
2013
+ import { SimAws } from "@kensio/yulin";
2014
+ import { serveSimAws } from "@kensio/yulin/serve";
2015
+
2016
+ const simAws = new SimAws();
2017
+ const srv = await serveSimAws({ simAws });
2018
+
2019
+ try {
2020
+ const stack = await simAws.cloudFormation().deployTemplate({
2021
+ stackName: "site-stack",
2022
+ template: {
2023
+ Resources: {
2024
+ SiteBucket: {
2025
+ Type: "AWS::S3::Bucket",
2026
+ Properties: {
2027
+ BucketName: "site-bucket",
2028
+ PublicAccessBlockConfiguration: {
2029
+ BlockPublicAcls: true,
2030
+ IgnorePublicAcls: true,
2031
+ },
2032
+ },
2033
+ },
2034
+ // The Origin reads the Bucket anonymously, so the site needs a policy
2035
+ // making it publicly readable.
2036
+ SiteBucketPolicy: {
2037
+ Type: "AWS::S3::BucketPolicy",
2038
+ DependsOn: "SiteBucket",
2039
+ Properties: {
2040
+ Bucket: "site-bucket",
2041
+ PolicyDocument: {
2042
+ Version: "2012-10-17",
2043
+ Statement: {
2044
+ Effect: "Allow",
2045
+ Principal: "*",
2046
+ Action: "s3:GetObject",
2047
+ Resource: "arn:aws:s3:::site-bucket/*",
2048
+ },
2049
+ },
2050
+ },
2051
+ },
2052
+ CacheHeaders: {
2053
+ Type: "AWS::CloudFront::ResponseHeadersPolicy",
2054
+ Properties: {
2055
+ ResponseHeadersPolicyConfig: {
2056
+ Name: "CacheHeaders",
2057
+ CustomHeadersConfig: {
2058
+ Items: [
2059
+ {
2060
+ Header: "Cache-Control",
2061
+ Override: true,
2062
+ Value: "public, max-age=0, must-revalidate",
2063
+ },
2064
+ ],
2065
+ },
2066
+ },
2067
+ },
2068
+ },
2069
+ SiteDistribution: {
2070
+ Type: "AWS::CloudFront::Distribution",
2071
+ DependsOn: ["SiteBucket", "CacheHeaders"],
2072
+ Properties: {
2073
+ DistributionConfig: {
2074
+ DefaultRootObject: "index.html",
2075
+ Origins: [
2076
+ {
2077
+ Id: "SiteOrigin",
2078
+ DomainName: "site-bucket.s3.amazonaws.com",
2079
+ S3OriginConfig: {},
2080
+ },
2081
+ ],
2082
+ DefaultCacheBehavior: {
2083
+ TargetOriginId: "SiteOrigin",
2084
+ ViewerProtocolPolicy: "allow-all",
2085
+ ResponseHeadersPolicyId: { Ref: "CacheHeaders" },
2086
+ },
2087
+ },
2088
+ },
2089
+ },
2090
+ },
2091
+ Outputs: {
2092
+ DistributionDomainName: {
2093
+ Value: { "Fn::GetAtt": ["SiteDistribution", "DomainName"] },
2094
+ },
2095
+ },
2096
+ },
2097
+ });
2098
+
2099
+ await stack.waitForDeployComplete();
2100
+
2101
+ await simAws.s3().putObject(
2102
+ new PutObjectCommand({
2103
+ Bucket: "site-bucket",
2104
+ Key: "index.html",
2105
+ ContentType: "text/html",
2106
+ Body: "<h1>Home</h1>",
2107
+ }),
2108
+ );
2109
+
2110
+ const domainName = stack.output("DistributionDomainName");
2111
+ const response = await fetch(srv.localUrl(`http://${domainName}/`));
2112
+
2113
+ console.log(response.headers.get("cache-control"));
2114
+ } finally {
2115
+ await srv.close();
2116
+ }
2117
+ ```
2118
+
2119
+ Each header in `CustomHeadersConfig` carries an `Override` boolean. With it set, the policy's value
2120
+ replaces one the Origin sent. Without it, the Origin's value is kept and the policy's is dropped. A
2121
+ header the Origin left out is added either way.
2122
+
2123
+ `RemoveHeadersConfig` takes headers away, and is applied before the added ones. A header named in
2124
+ both sections ends up present with the policy's value.
2125
+
2126
+ The policy is applied after a custom error response is fetched and before the viewer-response event,
2127
+ as CloudFront does. An error page carries the policy's headers. A viewer-response Function sees them
2128
+ in `event.response.headers` and can change them, where the Origin answered below 400 and the
2129
+ Function ran at all.
2130
+
2131
+ `SecurityHeadersConfig` is what CDK's `ResponseHeadersPolicy` construct synthesizes from
2132
+ `securityHeadersBehavior`, and every one of its sections is modelled. `ContentSecurityPolicy`,
2133
+ `ContentTypeOptions`, `FrameOptions`, `ReferrerPolicy`, `StrictTransportSecurity` and `XSSProtection`
2134
+ each become the header CloudFront documents for it, honouring the section's own `Override` the same
2135
+ way a `CustomHeadersConfig` item does.
2136
+
2137
+ `ServerTimingHeadersConfig` adds a `Server-Timing` header once `Enabled` is true. `SamplingRate` is
2138
+ ignored. This simulation adds the header to every response. A test asserting on it never depends on
2139
+ chance, and the header's value is a fixed placeholder in place of real Origin timing.
2140
+
2141
+ `CorsConfig` is what CDK's `corsBehavior` synthesizes. CloudFront reflects the viewer request's
2142
+ `Origin` header against `AccessControlAllowOrigins`, in place of sending the list itself. A request
2143
+ naming an Origin the list allows gets the CORS headers the section configures, with the response
2144
+ varying on `Origin` unless the list contains `*`. A request naming one the list omits gets none of
2145
+ them, matching CloudFront, which sends none in preference to a mismatched one.
2146
+ `AccessControlAllowMethods` of `["ALL"]` expands to CloudFront's full method list, and
2147
+ `AccessControlAllowCredentials: false` leaves `Access-Control-Allow-Credentials` off entirely, since
2148
+ a header naming `false` means the same as its absence to a browser.
2149
+
2150
+ `AccessControlAllowMethods`, `AccessControlAllowHeaders` and `AccessControlMaxAgeSec` answer what a
2151
+ preflight asks, and their headers go on a response to an `OPTIONS` request alone. Every other method
2152
+ is answered without them, as CloudFront answers it. `Access-Control-Allow-Origin`,
2153
+ `Access-Control-Allow-Credentials` and `Access-Control-Expose-Headers` come back either way, and so
2154
+ does the `Vary: Origin` a reflected Origin carries. A list left empty sends no header at all, which
2155
+ is how `SimpleCORS` answers with the Origin header by itself.
2156
+
2157
+ An allow-list entry may use the wildcard on its own, meaning every Origin, or as the leftmost
2158
+ subdomain, so `*.example.org` matches `https://site.example.org`. It stands for exactly one label, as
2159
+ a wildcard certificate does, and it leaves `https://deep.site.example.org` unmatched. An entry naming
2160
+ no scheme matches the host whichever scheme the request used. CloudFront allows the wildcard nowhere
2161
+ else, and an entry placing one elsewhere (`example.*`, `test.*.example.org`, `*test.example.org`,
2162
+ `exa*mple.org`) fails the stack.
2163
+
2164
+ `OriginOverride` decides the whole CORS section at once, where the `Override` on a custom or security
2165
+ header decides one header. Without it, an Origin response carrying any CORS header at all, named by
2166
+ the policy or otherwise, keeps every header the section would have set off the response.
2167
+
2168
+ ### Managed policies
2169
+
2170
+ CloudFront's five managed policies are here from the start, under the IDs AWS publishes, and a
2171
+ Behavior names one without a template creating anything. `SecurityHeadersPolicy`
2172
+ (`67f7725c-6f97-4210-82d7-5512b31e9d03`), `SimpleCORS` (`60669652-455b-4ae9-85a4-c4c02393f86c`),
2173
+ `CORS-With-Preflight` (`5cc3b908-e619-4b99-88e5-2cf7f45965bd`), `CORS-and-SecurityHeadersPolicy`
2174
+ (`e61eb60c-9c35-4d20-a928-2b84e02af89c`) and `CORS-with-preflight-and-SecurityHeadersPolicy`
2175
+ (`eaab4381-ed33-4a86-88ca-d9558dc6cd63`) each carry the sections
2176
+ [AWS documents](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-response-headers-policies.html)
2177
+ for it. CDK's `ResponseHeadersPolicy.SECURITY_HEADERS` and its four siblings synthesize those IDs, so
2178
+ a stack reaching for one deploys and serves the headers.
2179
+
2180
+ The managed policies sit in CloudFront's own namespace. A template may create a policy called
2181
+ `SecurityHeadersPolicy` of its own, and deleting that stack leaves the managed one where it was.
2182
+
2183
+ A CloudFormation Distribution whose Behavior names a policy that is neither managed nor created here
2184
+ deploys without one. The `ResponseHeadersPolicyId` lands on `stack.ignoredProperties` under that
2185
+ Behavior, and the Behavior serves every response without the headers the policy would have set. A
2186
+ template naming a policy from a real account, or one another stack created, is ordinary, and a site
2187
+ that failed to deploy over a set of headers would cost a local dev server and a test suite every
2188
+ request they make. One Behavior losing its policy leaves the others holding theirs, and a path
2189
+ Behavior is recorded under its `PathPattern`, the way a skipped Lambda@Edge association is.
2190
+
2191
+ `CreateDistribution` and `UpdateDistribution` still refuse the same ID, as real CloudFront refuses
2192
+ it.
2193
+
2194
+ ## Cache policies
2195
+
2196
+ A cache policy decides what a cache Behavior keys its cache on and how long an object stays there.
2197
+ Declare one as `AWS::CloudFront::CachePolicy` and point a Behavior's `CachePolicyId` at it with a
2198
+ `Ref`, which is what CDK's `CachePolicy` construct synthesizes.
2199
+
2200
+ ```typescript sim-cloudfront-cache-policy
2201
+ /**
2202
+ * Reading back the cache policy a Behavior was given.
2203
+ */
2204
+
2205
+ import { GetDistributionCommand } from "@aws-sdk/client-cloudfront";
2206
+ import { SimAws } from "@kensio/yulin";
2207
+
2208
+ const simAws = new SimAws();
2209
+
2210
+ const stack = await simAws.cloudFormation().deployTemplate({
2211
+ stackName: "site-stack",
2212
+ template: {
2213
+ Resources: {
2214
+ SiteBucket: {
2215
+ Type: "AWS::S3::Bucket",
2216
+ Properties: { BucketName: "site-bucket" },
2217
+ },
2218
+ BeaconPolicy: {
2219
+ Type: "AWS::CloudFront::CachePolicy",
2220
+ Properties: {
2221
+ CachePolicyConfig: {
2222
+ Name: "BeaconPolicy",
2223
+ MinTTL: 0,
2224
+ DefaultTTL: 0,
2225
+ MaxTTL: 0,
2226
+ ParametersInCacheKeyAndForwardedToOrigin: {
2227
+ EnableAcceptEncodingGzip: false,
2228
+ CookiesConfig: { CookieBehavior: "none" },
2229
+ HeadersConfig: { HeaderBehavior: "none" },
2230
+ QueryStringsConfig: { QueryStringBehavior: "none" },
2231
+ },
2232
+ },
2233
+ },
2234
+ },
2235
+ SiteDistribution: {
2236
+ Type: "AWS::CloudFront::Distribution",
2237
+ DependsOn: ["SiteBucket", "BeaconPolicy"],
2238
+ Properties: {
2239
+ DistributionConfig: {
2240
+ DefaultRootObject: "index.html",
2241
+ Origins: [
2242
+ {
2243
+ Id: "SiteOrigin",
2244
+ DomainName: "site-bucket.s3.amazonaws.com",
2245
+ S3OriginConfig: {},
2246
+ },
2247
+ ],
2248
+ DefaultCacheBehavior: {
2249
+ TargetOriginId: "SiteOrigin",
2250
+ ViewerProtocolPolicy: "allow-all",
2251
+ // CachingOptimized, one of CloudFront's managed policies.
2252
+ CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6",
2253
+ },
2254
+ CacheBehaviors: [
2255
+ {
2256
+ PathPattern: "/beacon",
2257
+ TargetOriginId: "SiteOrigin",
2258
+ ViewerProtocolPolicy: "allow-all",
2259
+ CachePolicyId: { Ref: "BeaconPolicy" },
2260
+ },
2261
+ ],
2262
+ },
2263
+ },
2264
+ },
2265
+ },
2266
+ Outputs: {
2267
+ DistributionId: { Value: { Ref: "SiteDistribution" } },
2268
+ BeaconPolicyId: { Value: { Ref: "BeaconPolicy" } },
2269
+ },
2270
+ },
2271
+ });
2272
+
2273
+ await stack.waitForDeployComplete();
2274
+
2275
+ const read = await simAws
2276
+ .cloudFront()
2277
+ .getDistribution(
2278
+ new GetDistributionCommand({ Id: stack.output("DistributionId") }),
2279
+ );
2280
+ const config = read.Distribution?.DistributionConfig;
2281
+
2282
+ // The managed ID the default Behavior was given.
2283
+ console.log(config?.DefaultCacheBehavior?.CachePolicyId);
2284
+
2285
+ // The ID of the policy the template created, which the path Behavior Refs.
2286
+ console.log(
2287
+ config?.CacheBehaviors?.Items?.[0]?.CachePolicyId ===
2288
+ stack.output("BeaconPolicyId"),
2289
+ );
2290
+ ```
2291
+
2292
+ A Behavior records the ID it was given, and `GetDistribution` reports it back for both the default
2293
+ Behavior and a named one. An update changing the policy is reported the same way.
2294
+
2295
+ Nothing here reads the policy itself. The TTLs, the cache key and the compression settings need a
2296
+ cache, and sim CloudFront holds none. Every request reaches the Origin whatever the policy would
2297
+ have cached on real CloudFront.
2298
+
2299
+ A policy name is unique within an account, as it is in CloudFront. A second
2300
+ `AWS::CloudFront::CachePolicy` claiming a name is refused with `CachePolicyAlreadyExists`.
2301
+ `Name` and `Comment` are the parts of `CachePolicyConfig` the policy carries.
2302
+
2303
+ ### Managed cache policies
2304
+
2305
+ CloudFront's seven managed policies are here from the start, under the IDs AWS publishes, and a
2306
+ Behavior names one without a template creating anything. `CachingOptimized`
2307
+ (`658327ea-f89d-4fab-a63d-7e88639e58f6`), `CachingDisabled` (`4135ea2d-6df8-44a3-9df3-4b5a84be39ad`),
2308
+ `CachingOptimizedForUncompressedObjects` (`b2884449-e4de-46a7-ac36-70bc7f1ddd6d`), `Amplify`
2309
+ (`2e54312d-136d-493c-8eb9-b001f22f67d2`), `Elemental-MediaPackage`
2310
+ (`08627262-05a9-4f76-9ded-b50ca2e3a84f`), `UseOriginCacheControlHeaders`
2311
+ (`83da9c7e-98b4-4e11-a168-04f0df8e2c65`) and `UseOriginCacheControlHeaders-QueryStrings`
2312
+ (`4cc15a8a-d715-48a4-82b8-cc0b614638fe`) are each held under the name
2313
+ [AWS publishes](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html)
2314
+ for it. CDK's `CachePolicy.CACHING_OPTIMIZED` and its six siblings synthesize those IDs. A stack
2315
+ reaching for one deploys.
2316
+
2317
+ The managed policies sit in CloudFront's own namespace. A template may create a policy called
2318
+ `CachingDisabled` of its own, and deleting that stack leaves the managed one where it was.
2319
+
2320
+ A CloudFormation Distribution whose Behavior names a policy that is neither managed nor created here
2321
+ deploys without one. The `CachePolicyId` lands on `stack.ignoredProperties` under that Behavior, the
2322
+ way an absent response headers policy does, and the Behavior reports no policy.
2323
+
2324
+ `CreateDistribution` and `UpdateDistribution` still refuse the same ID with `NoSuchCachePolicy`, as
2325
+ real CloudFront refuses it.
2326
+
2327
+ ## Origin access controls
2328
+
2329
+ An origin access control is how a Distribution authenticates to a private Origin. The Origin then
2330
+ admits the Distribution and nothing else. Declare one as `AWS::CloudFront::OriginAccessControl` and
2331
+ point an Origin's `OriginAccessControlId` at it with a `Ref`, which is what CDK's
2332
+ `S3BucketOrigin.withOriginAccessControl` synthesizes.
2333
+
2334
+ An `OriginAccessControlOriginType` of `s3` signs for an S3 Bucket Origin, and one of `lambda` signs
2335
+ for a Lambda Function URL Origin. The origin type has to match the Origin it is attached to. An `s3`
2336
+ origin access control on a custom Origin, or a `lambda` one on an S3 Origin, fails the Stack when
2337
+ the Distribution is created, as CloudFront refuses it.
2338
+
2339
+ An S3 Origin whose origin access control signs reads its Bucket as the `cloudfront.amazonaws.com`
2340
+ service principal, carrying the Distribution's ARN as `aws:SourceArn`. The Bucket policy is then the
2341
+ whole decision. The Bucket needs a statement granting `s3:GetObject` to that principal, conditioned
2342
+ on the Distribution allowed to read it. That is the policy CDK writes. A condition naming a different
2343
+ Distribution, or an Origin that was never given an origin access control, answers 403.
2344
+
2345
+ ```typescript sim-cloudfront-origin-access-control
2346
+ /**
2347
+ * Serving a private S3 Bucket through an origin access control.
2348
+ */
2349
+
2350
+ import { PutObjectCommand } from "@aws-sdk/client-s3";
2351
+
2352
+ import { SimAws } from "@kensio/yulin";
2353
+ import { serveSimAws } from "@kensio/yulin/serve";
2354
+
2355
+ const simAws = new SimAws();
2356
+ const srv = await serveSimAws({ simAws });
2357
+
2358
+ try {
2359
+ const stack = await simAws.cloudFormation().deployTemplate({
2360
+ stackName: "site-stack",
2361
+ template: {
2362
+ Resources: {
2363
+ SiteBucket: {
2364
+ Type: "AWS::S3::Bucket",
2365
+ Properties: { BucketName: "site-bucket" },
2366
+ },
2367
+ SiteOac: {
2368
+ Type: "AWS::CloudFront::OriginAccessControl",
2369
+ Properties: {
2370
+ OriginAccessControlConfig: {
2371
+ Name: "site-oac",
2372
+ OriginAccessControlOriginType: "s3",
2373
+ SigningBehavior: "always",
2374
+ SigningProtocol: "sigv4",
2375
+ },
2376
+ },
2377
+ },
2378
+ SiteDistribution: {
2379
+ Type: "AWS::CloudFront::Distribution",
2380
+ Properties: {
2381
+ DistributionConfig: {
2382
+ Enabled: true,
2383
+ DefaultRootObject: "index.html",
2384
+ Origins: [
2385
+ {
2386
+ Id: "SiteOrigin",
2387
+ DomainName: "site-bucket.s3.amazonaws.com",
2388
+ S3OriginConfig: {},
2389
+ OriginAccessControlId: { Ref: "SiteOac" },
2390
+ },
2391
+ ],
2392
+ DefaultCacheBehavior: {
2393
+ TargetOriginId: "SiteOrigin",
2394
+ ViewerProtocolPolicy: "allow-all",
2395
+ },
2396
+ },
2397
+ },
2398
+ },
2399
+ // Nothing but this Distribution may read the Bucket, which is what the
2400
+ // condition on the Distribution's ARN says.
2401
+ SiteBucketPolicy: {
2402
+ Type: "AWS::S3::BucketPolicy",
2403
+ Properties: {
2404
+ Bucket: { Ref: "SiteBucket" },
2405
+ PolicyDocument: {
2406
+ Version: "2012-10-17",
2407
+ Statement: [
2408
+ {
2409
+ Effect: "Allow",
2410
+ Principal: { Service: "cloudfront.amazonaws.com" },
2411
+ Action: "s3:GetObject",
2412
+ Resource: "arn:aws:s3:::site-bucket/*",
2413
+ Condition: {
2414
+ StringEquals: {
2415
+ "AWS:SourceArn": {
2416
+ "Fn::Join": [
2417
+ "",
2418
+ [
2419
+ "arn:aws:cloudfront::",
2420
+ { Ref: "AWS::AccountId" },
2421
+ ":distribution/",
2422
+ { Ref: "SiteDistribution" },
2423
+ ],
2424
+ ],
2425
+ },
2426
+ },
2427
+ },
2428
+ },
2429
+ ],
2430
+ },
2431
+ },
2432
+ },
2433
+ },
2434
+ Outputs: {
2435
+ SiteHostname: {
2436
+ Value: { "Fn::GetAtt": ["SiteDistribution", "DomainName"] },
2437
+ },
2438
+ },
2439
+ },
2440
+ });
2441
+
2442
+ await stack.waitForDeployComplete();
2443
+
2444
+ await simAws.s3().putObject(
2445
+ new PutObjectCommand({
2446
+ Bucket: "site-bucket",
2447
+ Key: "index.html",
2448
+ ContentType: "text/html",
2449
+ Body: "<h1>Home</h1>",
2450
+ }),
2451
+ );
2452
+
2453
+ const siteHostname = stack.output("SiteHostname");
2454
+ const home = await fetch(srv.localUrl(`http://${siteHostname}/`));
2455
+
2456
+ console.log(await home.text()); // <h1>Home</h1>
2457
+ } finally {
2458
+ await srv.close();
2459
+ }
2460
+ ```
2461
+
2462
+ The Bucket policy names the Distribution's ARN, and is created after the Distribution. The `Ref`
2463
+ inside `Fn::Join` is the dependency CloudFormation orders the Stack by. The read is settled per
2464
+ request, because the policy deciding it comes into existence after the Distribution does. The Origin
2465
+ works out who it is reading as each time.
2466
+
2467
+ ### A Lambda Function URL Origin
2468
+
2469
+ Putting a Function URL with `AuthType: AWS_IAM` behind a Distribution takes the origin access
2470
+ control with `OriginAccessControlOriginType: lambda`, a custom Origin naming it whose `DomainName`
2471
+ is the Function URL's hostname, and two `AWS::Lambda::Permission` Resources granting
2472
+ `cloudfront.amazonaws.com` for that Distribution. It is the only way to serve a Function URL through
2473
+ CloudFront without leaving the Function URL open to anyone who finds its endpoint.
2474
+
2475
+ Both permissions are needed. One grants `lambda:InvokeFunctionUrl` and the other
2476
+ `lambda:InvokeFunction`, to the same principal with the same `SourceArn`, as
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
+ sets out. CDK's `FunctionUrlOrigin.withOriginAccessControl` writes only the first. A CDK app has to
2479
+ add the second itself:
2480
+
2481
+ ```typescript
2482
+ greeterFunction.addPermission("InvokeFunctionFromCloudFront", {
2483
+ principal: new iam.ServicePrincipal("cloudfront.amazonaws.com"),
2484
+ action: "lambda:InvokeFunction",
2485
+ sourceArn: cdk.Fn.join("", [
2486
+ "arn:",
2487
+ cdk.Aws.PARTITION,
2488
+ ":cloudfront::",
2489
+ cdk.Aws.ACCOUNT_ID,
2490
+ ":distribution/",
2491
+ distribution.distributionId,
2492
+ ]),
2493
+ });
2494
+ ```
2495
+
2496
+ The Origin request is made as the `cloudfront.amazonaws.com` service principal carrying the
2497
+ Distribution's ARN, the same pair an S3 Origin read carries, and the function's resource policy is
2498
+ the whole decision. A Stack missing either permission, or with one naming a different Distribution,
2499
+ deploys and then answers 403 through the Distribution, as the real deployment does. The function is
2500
+ never invoked, and writes no logs to look at either.
2501
+
2502
+ ```typescript sim-cloudfront-function-url-origin-access-control
2503
+ /**
2504
+ * Serving a private Lambda Function URL through an origin access control.
2505
+ */
2506
+
2507
+ import { SimAws } from "@kensio/yulin";
2508
+ import { serveSimAws } from "@kensio/yulin/serve";
2509
+
2510
+ const simAws = new SimAws();
2511
+ const srv = await serveSimAws({ simAws });
2512
+
2513
+ try {
2514
+ const stack = await simAws.cloudFormation().deployTemplate({
2515
+ stackName: "greeter-stack",
2516
+ template: {
2517
+ Resources: {
2518
+ GreeterFunction: {
2519
+ Type: "AWS::Lambda::Function",
2520
+ Properties: {
2521
+ FunctionName: "greeter",
2522
+ Role: "arn:aws:iam::888888888888:role/GreeterRole",
2523
+ Handler: "index.handler",
2524
+ Runtime: "nodejs22.x",
2525
+ Code: {
2526
+ ZipFile:
2527
+ "exports.handler = async () => " +
2528
+ "({ statusCode: 200, body: 'Hello from behind CloudFront' });",
2529
+ },
2530
+ },
2531
+ },
2532
+ GreeterUrl: {
2533
+ Type: "AWS::Lambda::Url",
2534
+ Properties: {
2535
+ TargetFunctionArn: { "Fn::GetAtt": ["GreeterFunction", "Arn"] },
2536
+ AuthType: "AWS_IAM",
2537
+ },
2538
+ },
2539
+ GreeterOac: {
2540
+ Type: "AWS::CloudFront::OriginAccessControl",
2541
+ Properties: {
2542
+ OriginAccessControlConfig: {
2543
+ Name: "greeter-oac",
2544
+ OriginAccessControlOriginType: "lambda",
2545
+ SigningBehavior: "always",
2546
+ SigningProtocol: "sigv4",
2547
+ },
2548
+ },
2549
+ },
2550
+ GreeterDistribution: {
2551
+ Type: "AWS::CloudFront::Distribution",
2552
+ Properties: {
2553
+ DistributionConfig: {
2554
+ Enabled: true,
2555
+ Origins: [
2556
+ {
2557
+ Id: "GreeterOrigin",
2558
+ // An Origin takes a domain name, and the Function URL
2559
+ // attribute is a URL, so the host comes out of it.
2560
+ DomainName: {
2561
+ "Fn::Select": [
2562
+ 2,
2563
+ {
2564
+ "Fn::Split": [
2565
+ "/",
2566
+ { "Fn::GetAtt": ["GreeterUrl", "FunctionUrl"] },
2567
+ ],
2568
+ },
2569
+ ],
2570
+ },
2571
+ CustomOriginConfig: { OriginProtocolPolicy: "https-only" },
2572
+ OriginAccessControlId: { Ref: "GreeterOac" },
2573
+ },
2574
+ ],
2575
+ DefaultCacheBehavior: {
2576
+ TargetOriginId: "GreeterOrigin",
2577
+ ViewerProtocolPolicy: "allow-all",
2578
+ },
2579
+ },
2580
+ },
2581
+ },
2582
+ // Nothing but this Distribution may invoke the Function URL, which is
2583
+ // what the condition on the Distribution's ARN says. Reaching the URL
2584
+ // takes both actions, so leaving either one out is a 403.
2585
+ InvokeFunctionUrlFromCloudFront: {
2586
+ Type: "AWS::Lambda::Permission",
2587
+ Properties: {
2588
+ FunctionName: { "Fn::GetAtt": ["GreeterFunction", "Arn"] },
2589
+ Action: "lambda:InvokeFunctionUrl",
2590
+ Principal: "cloudfront.amazonaws.com",
2591
+ SourceArn: {
2592
+ "Fn::Join": [
2593
+ "",
2594
+ [
2595
+ "arn:aws:cloudfront::",
2596
+ { Ref: "AWS::AccountId" },
2597
+ ":distribution/",
2598
+ { Ref: "GreeterDistribution" },
2599
+ ],
2600
+ ],
2601
+ },
2602
+ },
2603
+ },
2604
+ InvokeFunctionFromCloudFront: {
2605
+ Type: "AWS::Lambda::Permission",
2606
+ Properties: {
2607
+ FunctionName: { "Fn::GetAtt": ["GreeterFunction", "Arn"] },
2608
+ Action: "lambda:InvokeFunction",
2609
+ Principal: "cloudfront.amazonaws.com",
2610
+ SourceArn: {
2611
+ "Fn::Join": [
2612
+ "",
2613
+ [
2614
+ "arn:aws:cloudfront::",
2615
+ { Ref: "AWS::AccountId" },
2616
+ ":distribution/",
2617
+ { Ref: "GreeterDistribution" },
2618
+ ],
2619
+ ],
2620
+ },
2621
+ },
2622
+ },
2623
+ },
2624
+ Outputs: {
2625
+ SiteHostname: {
2626
+ Value: { "Fn::GetAtt": ["GreeterDistribution", "DomainName"] },
2627
+ },
2628
+ },
2629
+ },
2630
+ });
2631
+
2632
+ await stack.waitForDeployComplete();
2633
+
2634
+ const siteHostname = stack.output("SiteHostname");
2635
+ const greeting = await fetch(srv.localUrl(`http://${siteHostname}/greeting`));
2636
+
2637
+ console.log(await greeting.text()); // Hello from behind CloudFront
2638
+ } finally {
2639
+ await srv.close();
2640
+ }
2641
+ ```
2642
+
2643
+ The Function URL is reachable directly as well, on its own endpoint, and it refuses a request that
2644
+ arrives there without the permission the Distribution has. That is the point of the auth type. The
2645
+ endpoint exists, and only the Distribution may use it.
2646
+
2647
+ `SigningBehavior` takes any of `always`, `never` and `no-override`. `always` and `no-override` both
2648
+ sign, since nothing here sends a pre-signed viewer request to an Origin for `no-override` to pass
2649
+ through. `never` turns the origin access control off while leaving it in place, and the Origin is
2650
+ reached anonymously, as an Origin with no origin access control is. An S3 Origin then needs a Bucket
2651
+ policy allowing that, and an `AWS_IAM` Function URL refuses the request outright.
2652
+
2653
+ `Ref` and `Fn::GetAtt` on `Id` both return the ID, so either resolves an Origin's
2654
+ `OriginAccessControlId`. An Origin naming an ID no origin access control holds is refused with
2655
+ `InvalidOriginAccessControl` when the Distribution is created. Tearing the Stack down removes the
2656
+ origin access control, and its name is free again.
2657
+
2658
+ `OriginAccessControlOriginType` must be `s3` or `lambda`, and `SigningProtocol` must be `sigv4`. Any
2659
+ other value fails the Stack by name.
2660
+
2661
+ A CloudFormation template is the only way to make one. There is no `CreateOriginAccessControl`
2662
+ command here.
2663
+
2664
+ #### Posting to a Function URL Origin
2665
+
2666
+ A POST or PUT through an origin access control has to carry the SHA-256 of its body in an
2667
+ `x-amz-content-sha256` header. CloudFront streams the viewer's body on to the Origin without
2668
+ buffering it, and has no hash of its own to sign with. It signs the hash the viewer declared, and
2669
+ `UNSIGNED-PAYLOAD` where the viewer declared none. Lambda supports no unsigned payload, and answers
2670
+ `403` with `The request signature we calculated does not match the signature you provided`. The
2671
+ handler never runs. The declared hash is checked against the body that arrived, and a digest of
2672
+ other bytes is refused the same way.
2673
+
2674
+ A viewer computes the digest of what it is about to send, the way any SigV4 client does:
2675
+
2676
+ ```typescript
2677
+ const body = JSON.stringify({ email: "someone@example.com" });
2678
+ const response = await fetch(`http://${siteHostname}/sign-in`, {
2679
+ method: "POST",
2680
+ body,
2681
+ headers: {
2682
+ "content-type": "application/json",
2683
+ "x-amz-content-sha256": createHash("sha256").update(body).digest("hex"),
2684
+ },
2685
+ });
2686
+ ```
2687
+
2688
+ A GET or a HEAD is left alone. SigV4 hashes an empty payload for a request without a body, and
2689
+ CloudFront can sign one of those on its own. An origin access control with a `SigningBehavior` of
2690
+ `never` signs no Origin request, and states no payload hash for one. A POST through one reaches
2691
+ the Origin anonymously, as it did before.
2692
+
2693
+ AWS documents the requirement on
2694
+ [Restrict access to an AWS Lambda function URL origin](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-lambda.html).
2695
+ A simulated Distribution refuses the request for the same reason a real one does. A form post
2696
+ missing the header fails in a test as well as on the deployment.
2697
+
2698
+ ## Key value stores
2699
+
2700
+ A key value store holds data a CloudFront Function reads at request time. A redirect table or a
2701
+ feature flag can live there instead of being baked into the Function's code.
2702
+
2703
+ AWS splits this across two SDK clients, and so does the simulator. The CloudFront client owns the
2704
+ store, through `CreateKeyValueStoreCommand`, `DescribeKeyValueStoreCommand`,
2705
+ `ListKeyValueStoresCommand`, `UpdateKeyValueStoreCommand` and `DeleteKeyValueStoreCommand`, all
2706
+ addressing a store by name. The key value store client owns the data, through `GetKeyCommand`,
2707
+ `PutKeyCommand`, `DeleteKeyCommand`, `ListKeysCommand`, `UpdateKeysCommand` and its own
2708
+ `DescribeKeyValueStoreCommand`, all addressing a store by ARN.
2709
+
2710
+ Both clients are intercepted by `SimSdk`. Used directly, they are `simAws.cloudFront().keyValueStores()`
2711
+ and `simAws.cloudFrontKeyValueStore()`.
2712
+
2713
+ ```typescript sim-cloudfront-key-value-store
2714
+ /**
2715
+ * Creating a CloudFront key value store and writing keys to it.
2716
+ */
2717
+
2718
+ import { CreateKeyValueStoreCommand } from "@aws-sdk/client-cloudfront";
2719
+ import {
2720
+ DescribeKeyValueStoreCommand,
2721
+ GetKeyCommand,
2722
+ UpdateKeysCommand,
2723
+ } from "@aws-sdk/client-cloudfront-keyvaluestore";
2724
+
2725
+ import { SimAws } from "@kensio/yulin";
2726
+
2727
+ const simAws = new SimAws();
2728
+
2729
+ // The CloudFront client owns the store itself.
2730
+ const created = await simAws
2731
+ .cloudFront()
2732
+ .keyValueStores()
2733
+ .createKeyValueStore(
2734
+ new CreateKeyValueStoreCommand({
2735
+ Name: "redirects",
2736
+ Comment: "Where old paths go",
2737
+ }),
2738
+ );
2739
+
2740
+ const kvsArn = created.KeyValueStore.ARN;
2741
+ const data = simAws.cloudFrontKeyValueStore();
2742
+
2743
+ // The key value store client owns the data, and addresses the store by ARN.
2744
+ // Every write carries an ETag, and it is this API's own: the one the
2745
+ // CloudFront client returned above versions the resource, not the keys.
2746
+ const described = await data.describeKeyValueStore(
2747
+ new DescribeKeyValueStoreCommand({ KvsARN: kvsArn }),
2748
+ );
2749
+
2750
+ const written = await data.updateKeys(
2751
+ new UpdateKeysCommand({
2752
+ KvsARN: kvsArn,
2753
+ IfMatch: described.ETag,
2754
+ Puts: [
2755
+ { Key: "/old-page", Value: "/new-page" },
2756
+ { Key: "/legacy", Value: "/current" },
2757
+ ],
2758
+ }),
2759
+ );
2760
+
2761
+ console.log(written.ItemCount); // 2
2762
+
2763
+ const read = await data.getKey(
2764
+ new GetKeyCommand({ KvsARN: kvsArn, Key: "/old-page" }),
2765
+ );
2766
+
2767
+ console.log(read.Value); // /new-page
2768
+ ```
2769
+
2770
+ A new store is `PROVISIONING` when the command returns and becomes `READY` in the background, as in
2771
+ CloudFront. `await simAws.backgroundTasksComplete()` waits for that.
2772
+
2773
+ ### ETags
2774
+
2775
+ The key value store commands do check `IfMatch`, where the Distribution and Function commands ignore
2776
+ it. Both APIs require it on every write and CloudFront refuses a stale one, which is what stops two
2777
+ writers overwriting each other. A write carrying a stale ETag is refused with `PreconditionFailed`,
2778
+ and a caller has to thread the ETag through the way it does against CloudFront. Each write returns
2779
+ the new ETag for the next one.
2780
+
2781
+ A store has two ETags and they are not interchangeable, as in AWS. Each `DescribeKeyValueStore`
2782
+ returns its own. The CloudFront client's versions the store's configuration, and the key value store
2783
+ client's versions the keys. Writing a key leaves the configuration's ETag where it was, and changing
2784
+ the comment leaves the keys' where it was. A write carrying the other API's ETag is refused, and the
2785
+ message says which of the two it wanted.
2786
+
2787
+ ### Reading a store from a CloudFront Function
2788
+
2789
+ A Function reads its store through `cf`, which it gets from `import cf from "cloudfront"`. That is
2790
+ the one import JS 2.0 has. `cf.kvs()` opens the store the Function is associated with, and its
2791
+ `get`, `exists` and `meta` are all promises. A Function that reads a store is async.
2792
+
2793
+ A Function names the store it may read with `KeyValueStoreAssociations` on its `FunctionConfig`.
2794
+ CloudFront takes at most one, and only on `cloudfront-js-2.0`. An association on the 1.0 runtime is
2795
+ refused, because that runtime has no `cf` to reach a store through.
2796
+
2797
+ ```typescript sim-cloudfront-function-key-value-store
2798
+ /**
2799
+ * Reading a key value store from a CloudFront Function.
2800
+ */
2801
+
2802
+ import {
2803
+ CreateFunctionCommand,
2804
+ CreateKeyValueStoreCommand,
2805
+ } from "@aws-sdk/client-cloudfront";
2806
+ import {
2807
+ DescribeKeyValueStoreCommand,
2808
+ PutKeyCommand,
2809
+ } from "@aws-sdk/client-cloudfront-keyvaluestore";
2810
+
2811
+ import { SimAws } from "@kensio/yulin";
2812
+
2813
+ const simAws = new SimAws();
2814
+
2815
+ const created = await simAws
2816
+ .cloudFront()
2817
+ .keyValueStores()
2818
+ .createKeyValueStore(new CreateKeyValueStoreCommand({ Name: "redirects" }));
2819
+
2820
+ const kvsArn = created.KeyValueStore.ARN;
2821
+ const data = simAws.cloudFrontKeyValueStore();
2822
+
2823
+ const described = await data.describeKeyValueStore(
2824
+ new DescribeKeyValueStoreCommand({ KvsARN: kvsArn }),
2825
+ );
2826
+
2827
+ await data.putKey(
2828
+ new PutKeyCommand({
2829
+ KvsARN: kvsArn,
2830
+ Key: "/old-page",
2831
+ Value: "/new-page",
2832
+ IfMatch: described.ETag,
2833
+ }),
2834
+ );
2835
+
2836
+ // The Function names the store it may read. It gets `cf` from the one import
2837
+ // JS 2.0 has, and the read is awaited, so the handler is async.
2838
+ await simAws.cloudFront().createFunction(
2839
+ new CreateFunctionCommand({
2840
+ Name: "redirect-cff",
2841
+ FunctionConfig: {
2842
+ Comment: "Redirects from a key value store",
2843
+ Runtime: "cloudfront-js-2.0",
2844
+ KeyValueStoreAssociations: {
2845
+ Quantity: 1,
2846
+ Items: [{ KeyValueStoreARN: kvsArn }],
2847
+ },
2848
+ },
2849
+ FunctionCode: Buffer.from(`
2850
+ import cf from "cloudfront";
2851
+
2852
+ async function handler(event) {
2853
+ const request = event.request;
2854
+
2855
+ if (await cf.kvs().exists(request.uri)) {
2856
+ const target = await cf.kvs().get(request.uri);
2857
+
2858
+ return {
2859
+ statusCode: 302,
2860
+ statusDescription: "Found",
2861
+ headers: { location: { value: target } },
2862
+ };
2863
+ }
2864
+
2865
+ return request;
2866
+ }
2867
+ `),
2868
+ }),
2869
+ );
2870
+
2871
+ const cff = simAws.cloudFront().getCloudFrontFunctionByName("redirect-cff");
2872
+
2873
+ const redirected = await cff!.handleViewerRequest(
2874
+ new Request("https://cdn.test/old-page"),
2875
+ );
2876
+
2877
+ console.log((redirected as Response).status); // 302
2878
+ console.log((redirected as Response).headers.get("location")); // /new-page
2879
+ ```
2880
+
2881
+ `get` reads a string by default, and takes `{ format: "json" }` to parse the stored string or
2882
+ `{ format: "bytes" }` for its UTF-8 bytes. A missing key rejects. A Function that wants a default checks
2883
+ `exists` first, as the example does.
2884
+
2885
+ A Function written as a function reference has no import to write, and reads `cf` as a global.
2886
+ Importing `@kensio/yulin/cloudfront/globals` gives that global a type, along with the CloudFront
2887
+ Function event types. Each invocation gets its own `cf` through Node.js asynchronous context, and two
2888
+ Functions associated with different stores read their own even when they run at the same time.
2889
+
2890
+ ### From CloudFormation
2891
+
2892
+ `AWS::CloudFront::KeyValueStore` creates a store, and a Function associates one with
2893
+ `FunctionConfig.KeyValueStoreAssociations`. CloudFormation takes a plain array there, where the SDK
2894
+ takes a `Quantity` and `Items` pair. `Ref` on a key value store is its ARN, and the two fit together
2895
+ directly:
2896
+
2897
+ ```yaml
2898
+ Redirects:
2899
+ Type: AWS::CloudFront::KeyValueStore
2900
+ Properties:
2901
+ Name: redirects
2902
+
2903
+ RedirectFunction:
2904
+ Type: AWS::CloudFront::Function
2905
+ Properties:
2906
+ Name: redirect-cff
2907
+ AutoPublish: true
2908
+ FunctionCode: !Sub "..."
2909
+ FunctionConfig:
2910
+ Comment: Redirects from a key value store
2911
+ Runtime: cloudfront-js-2.0
2912
+ KeyValueStoreAssociations:
2913
+ - KeyValueStoreARN: !Ref Redirects
2914
+ ```
2915
+
2916
+ `Fn::GetAtt` supports `Arn`, `Id` and `Status`. Deleting the Stack deletes the store, after the
2917
+ Functions holding it have gone.
2918
+
2919
+ CDK's `cloudfront.KeyValueStore` and the `keyValueStore` prop on `cloudfront.Function` both deploy.
2920
+ A CDK stack needs no hand-editing.
2921
+
2922
+ `cf.kvs()` refuses when the Function is associated with no store, and refuses an ID belonging to some
2923
+ other store. Handing back an empty store would let a Function that lost its association run to
2924
+ completion and quietly take every default.
2925
+
2926
+ ## Available functionality
2927
+
2928
+ Sim CloudFront currently supports:
2929
+
2930
+ - `CreateDistributionCommand`, `GetDistributionCommand`, `UpdateDistributionCommand` and
2931
+ `DeleteDistributionCommand`
2932
+ - `CreateFunctionCommand`, `ListFunctionsCommand`, `DescribeFunctionCommand`, `GetFunctionCommand`
2933
+ and `DeleteFunctionCommand`
2934
+ - Refusing Function code over CloudFront's 10 KB size limit, with `FunctionSizeLimitExceeded`
2935
+ - Key value stores, through both the CloudFront client and the key value store data client
2936
+ - S3 Origins backed by sim S3 Buckets, reading them as the Bucket policy allows
2937
+ - Custom Origins reaching sim HTTP APIs and sim Lambda Function URLs in process
2938
+ - `CustomHeaders` and `OriginCustomHeaders` on an Origin, for an origin that admits only CloudFront
2939
+ - CloudFront Distribution hostnames such as `distro123.cloudfront.net`
2940
+ - Default cache Behavior and path-based cache Behaviors
2941
+ - `DefaultRootObject` and `CustomErrorResponses`, for static sites and single-page apps
2942
+ - `viewer-request` and `viewer-response` CloudFront Functions, including async ones
2943
+ - Lambda@Edge functions at all four events, through `LambdaFunctionAssociations`
2944
+ - `LambdaFunctionAssociations` on a template's Distribution, and CDK's `edgeLambdas`
2945
+ - CloudFront Functions reading an associated key value store through `cf.kvs()`
2946
+ - `AWS::CloudFront::ResponseHeadersPolicy`, for headers a cache Behavior sets on every response
2947
+ - `AWS::CloudFront::CachePolicy`, and a Behavior's `CachePolicyId` read back through `GetDistribution`
2948
+ - `AWS::CloudFront::KeyValueStore`, and `KeyValueStoreAssociations` on `AWS::CloudFront::Function`
2949
+ - `AWS::CloudFront::OriginAccessControl`, letting an Origin read a private Bucket as CloudFront
2950
+ - Viewer certificates from sim ACM, including CloudFront's `us-east-1` requirement
2951
+ - `WebACLId`, putting a simulated WAFv2 web ACL in front of everything a Distribution serves
2952
+ - Serving simulated CloudFront traffic on localhost with `serveSimAws`
2953
+
2954
+ The simulator focuses on useful behaviour for tests and local development, ahead of full CloudFront
2955
+ feature parity. Unsupported CloudFront options may be ignored or may throw errors depending on
2956
+ whether the simulator needs them to model the requested behaviour safely.
2957
+
2958
+ ## Limitations
2959
+
2960
+ Where sim CloudFront knowingly behaves differently from AWS:
2961
+
2962
+ - **The origin events run on every request that reaches the Origin.** Real CloudFront runs
2963
+ `origin-request` and `origin-response` on a cache miss, and serves a cache hit without reaching
2964
+ either. Simulated CloudFront holds no cache, and every request that gets as far as the Origin is a
2965
+ miss here. A request a web ACL blocked or a viewer-request function answered reaches neither
2966
+ event, and an `origin-request` function that returns a response leaves the Origin unread with no
2967
+ `origin-response` event after it.
2968
+ - **An Origin keeps its kind and its Bucket through an origin-request function.** Real CloudFront
2969
+ lets a handler hand back `origin.s3` where it was given `origin.custom`, or point an S3 Origin at
2970
+ another Bucket. Both need something a simulated Origin does not hold, the dispatcher that reaches
2971
+ a custom Origin and the Bucket a domain name resolved to when the Distribution was written. Each
2972
+ is refused with the 502 a failed edge function gets, carrying the reason. The domain name, the
2973
+ Origin path and the custom headers are the parts a handler can rewrite.
2974
+ - **A custom Origin reports CloudFront's default connection settings.** `keepaliveTimeout`,
2975
+ `port`, `protocol`, `readTimeout` and `sslProtocols` are what an origin event carries for every
2976
+ custom Origin, whatever `CustomOriginConfig` said, and a handler writing them changes nothing
2977
+ about the fetch. Nothing here opens a socket for them to apply to. The `customHeaders` of an S3
2978
+ Origin are empty for the same kind of reason. An S3 Origin reads its Bucket through GetObject and
2979
+ builds no request for a header to travel on.
2980
+ - **A custom error page is fetched without the origin events.** CloudFront fetches
2981
+ `ResponsePagePath` from the Origin, and an origin function runs for that fetch as it does for any
2982
+ other. Here the page is fetched directly. An `origin-request` function that rewrote the Origin
2983
+ leaves the error page coming from the Behavior's own Origin.
2984
+ - **A CDK `EdgeFunction` outside us-east-1 wants the whole cloud assembly.**
2985
+ `cloudfront.experimental.EdgeFunction` writes the function into a us-east-1 support stack and
2986
+ reads its ARN back through a custom resource in the stack that uses it. `deployCdkOut` deploys
2987
+ both stacks and the read finds the ARN. `deployTemplateFile` on the using stack's template alone
2988
+ deploys one of them, the read finds nothing, and the Distribution goes up without the
2989
+ association, recorded on `stack.ignoredProperties`. `cdk deploy` deploys both either way.
2990
+ - **Nothing is replicated.** Real Lambda@Edge copies the function out to every Region and creates the
2991
+ `AWSServiceRoleForLambdaReplicator` service-linked role to do it. Here the function is invoked
2992
+ where it was created. The trust policy and the `lambda:GetFunction` and `lambda:EnableReplication`
2993
+ permissions a real association needs are still checked, because those are what a first deploy
2994
+ fails on.
2995
+ - **A Lambda@Edge body is never truncated.** CloudFront caps the body it sends a `viewer-request`
2996
+ function and reports `inputTruncated` when it had to cut one. Every simulated body arrives whole
2997
+ and `inputTruncated` is always false, so a test finds out nothing about whether its request would
2998
+ be too large for a real edge function.
2999
+ - **The Origin's status decides whether a viewer-response function runs.** CloudFront skips the
3000
+ viewer-response event once the Origin answers 400 or higher, and both kinds of function are
3001
+ skipped here on that rule. Where the status is replaced further down the pipeline, by a custom
3002
+ error response or by an `origin-response` function, the Origin's own status still decides. AWS
3003
+ documents the restriction against the Origin's status and says nothing about the status something
3004
+ else puts in its place. A Distribution combining the two is where this simulation is guessing. A
3005
+ response an `origin-request` function generated has no Origin status behind it, and its own status
3006
+ stands in.
3007
+ - **CloudFront's disallowed and read-only header lists go unchecked.** Real CloudFront answers 502
3008
+ when an edge function adds `Connection` or edits `Content-Length`. Both kinds of function here
3009
+ write what they like, apart from the viewer-request `host`, which is restored.
3010
+ - **An S3 Origin with no origin access control reads its Bucket anonymously.** That is the unsigned
3011
+ request real CloudFront sends to the S3 REST endpoint without one. The Bucket policy has to make
3012
+ an Object publicly readable for the Distribution to serve it. A legacy
3013
+ `S3OriginConfig.OriginAccessIdentity` is refused by name. It signs the Origin request as a
3014
+ CloudFront canonical user nothing here models, and a Bucket policy written for one would deny the
3015
+ read in silence.
3016
+ - **A signed Origin request carries no signature.** An Origin whose origin access control signs
3017
+ reaches the Origin as the `cloudfront.amazonaws.com` service principal carrying the Distribution's
3018
+ ARN. That pair is what the Bucket policy or the function's resource policy is evaluated against,
3019
+ and no SigV4 signature is computed or checked. A Function URL Origin is told who the request is
3020
+ from at the simulated HTTP boundary, the same way anything else calling into simulated AWS in
3021
+ process says who it is. No other simulated request is signed here either, and the signature
3022
+ itself is beyond what a test can assert on. The payload hash is the one part of a signature that
3023
+ is stated and checked, because a Function URL turns a POST away over it. See
3024
+ [posting to a Function URL Origin](#posting-to-a-function-url-origin).
3025
+ - **An origin access control signs for an S3 or Lambda Function URL Origin only.** CloudFront also
3026
+ signs for MediaStore and MediaPackage V2 Origins, and both are left out. An
3027
+ `OriginAccessControlOriginType` other than `s3` or `lambda`, or a `SigningProtocol` other than
3028
+ `sigv4`, fails the Stack by naming the value. Neither is quietly treated as one of the two.
3029
+ - **An origin access control name is unique, and that is the whole of the checking.** A second one
3030
+ claiming a name is refused with `OriginAccessControlAlreadyExists`, as CloudFront refuses one.
3031
+ - **An origin access control has no command surface.** `CreateOriginAccessControl` and its siblings
3032
+ are absent, and `AWS::CloudFront::OriginAccessControl` is the only way to make one.
3033
+ - **A list's `Quantity` is only checked when it is there.** Every CloudFront list carries a count
3034
+ alongside its items, and a `Quantity` that disagrees with `Items` is refused with
3035
+ `InconsistentQuantities`, as CloudFront refuses it. A list arriving as a plain array, which is the
3036
+ CloudFormation shape, has no count to disagree with, and a template goes unchecked this way. So
3037
+ does a hand-written `{ Items: [...] }` with the count left out. The AWS SDK types make omitting
3038
+ `Quantity` a compile error, so what arrives without one is a different mistake from the one this
3039
+ catches.
3040
+ - **A web ACL a Distribution names has to exist here.** `WebACLId` resolves to a web ACL created in
3041
+ this simulation, and the ARN carries the Account and Region holding it. A managed web ACL, or one
3042
+ from a real account, is refused at create and at update. A CloudFormation Distribution is the
3043
+ exception and deploys without it, recording the property. Deleting a web ACL a Distribution still
3044
+ names leaves the Distribution answering `InvalidWebACLId` on every request, because real WAF
3045
+ refuses that deletion and nothing here tracks the association to refuse it.
3046
+ - **`IfMatch` ETags are ignored on a Distribution or a Function.** `UpdateDistributionCommand`,
3047
+ `DeleteDistributionCommand` and `DeleteFunctionCommand` all accept `IfMatch` and ignore it,
3048
+ leaving both `PreconditionFailed` and `InvalidIfMatchVersion` unused there. A stale ETag there
3049
+ costs a retry. The key value store commands are the exception and do check it, because the data
3050
+ API is built around it, and two writers racing on one store is the case it exists to catch.
3051
+ - **A key value store has no size quota.** CloudFront caps a store's total size and the length of a
3052
+ single key and value, and refuses a write that would exceed either. Nothing here counts against a
3053
+ quota, and `TotalSizeInBytes` is reported without being enforced. A test can find out nothing
3054
+ about whether its data would be too large for a real store.
3055
+ - **A key value store association is fixed once the Function is created.** There is no
3056
+ `UpdateFunction` here, and the store a Function reads is the one it was created with. Delete the
3057
+ Function and create it again to change it.
3058
+ - **A bound handler goes unmeasured.** Function code over CloudFront's 10 KB limit is refused with
3059
+ `FunctionSizeLimitExceeded`, counted on the source as uploaded. A handler passed as a function
3060
+ reference, through `makeCffFunctionCodeInput` or a CloudFormation binding, carries no source to
3061
+ count. The limit reaches only the inline code a real deploy would upload.
3062
+ - **`ImportSource` is unsupported.** `CreateKeyValueStoreCommand` ignores it, and
3063
+ `AWS::CloudFront::KeyValueStore` refuses a Resource carrying one. Nothing here reads an S3 Object
3064
+ as key data, and deploying an empty store would let a test pass against data the deploy should
3065
+ have seeded. Write the keys with `PutKey` or `UpdateKeys`.
3066
+ - **A `Status` Output holds the status at deploy time.** CloudFormation Outputs are resolved once,
3067
+ while a new store is still `PROVISIONING`, so `Fn::GetAtt` on `Status` in an Output reads
3068
+ `PROVISIONING` even though the store goes on to become `READY`. Read the store itself for its
3069
+ current status.
3070
+ - **Key listing is unpaginated.** `ListKeysCommand` and `ListKeyValueStoresCommand` answer with
3071
+ everything and never set a `NextToken` or `NextMarker`, leaving a test with no paging loop to
3072
+ exercise.
3073
+ - **A deletion goes ahead without waiting for the disable to deploy.** Real CloudFront needs the
3074
+ disabled Distribution to reach `Deployed` before it accepts the deletion. Here, `Enabled: false`
3075
+ is enough.
3076
+ - **A disabled Distribution still serves requests.** Real CloudFront answers a disabled Distribution
3077
+ with a 403. Only deleting a Distribution stops it serving here.
3078
+ - **`DeleteFunctionCommand` never answers `FunctionInUse`.** A CloudFront Function is never told
3079
+ that a cache Behavior has taken it up, and every Function is deletable. A Behavior left pointing at a
3080
+ deleted Function runs no Function code.
3081
+ - **A response headers policy name is unique, and that is the whole of the checking.** A second
3082
+ policy claiming a name is refused with `ResponseHeadersPolicyAlreadyExists`, as CloudFront refuses
3083
+ one. The header names and values themselves are stored as written.
3084
+ - **A response headers policy has no command surface.** `CreateResponseHeadersPolicy` and its
3085
+ siblings are absent, and `AWS::CloudFront::ResponseHeadersPolicy` is the only way to make one.
3086
+ - **`ServerTimingHeadersConfig` always adds the header once enabled.** `SamplingRate` decides what
3087
+ share of real responses carry `Server-Timing`. This simulation adds it to every response once
3088
+ `Enabled` is true. A test asserting on it never depends on chance. The header's value is a
3089
+ fixed placeholder, since nothing here measures an Origin fetch the way CloudFront's edge does.
3090
+ - **A cache policy is recorded and never applied.** Sim CloudFront models no edge caching. A
3091
+ Behavior's `CachePolicyId` is checked against the policies this simulation holds and reported
3092
+ back, and the TTLs, the cache key and the compression settings behind it decide nothing. Every
3093
+ request reaches the Origin, whatever the policy would have cached on real CloudFront.
3094
+ - **A cache policy carries its name and its comment, and nothing else.** The TTLs and
3095
+ `ParametersInCacheKeyAndForwardedToOrigin` of an `AWS::CloudFront::CachePolicy` are read past,
3096
+ since nothing here would act on them.
3097
+ - **`OriginRequestPolicyId` is accepted and ignored.** A Behavior's origin request policy is left
3098
+ unvalidated, and `AWS::CloudFront::OriginRequestPolicy` is skipped.