@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,3719 @@
1
+ # Simulated Lambda
2
+
3
+ Yulin includes a simulated AWS Lambda service for tests and local development. Functions are created
4
+ and invoked in-process and in memory, with no containers and no real AWS infrastructure.
5
+
6
+ Handlers run with their execution role as the simulated caller, so AWS calls made inside a handler
7
+ are authorized by simulated IAM, as on real Lambda. Creating a function and changing its role are
8
+ authorized as well, both against `lambda:` actions and against `iam:PassRole` on the execution role
9
+ the request names. See [passing a Role to a service](https://yulinsim.dev/services/iam/#passing-a-role-to-a-service)
10
+ in the IAM docs.
11
+
12
+ Lambda-specific helpers are imported from the `@kensio/yulin/lambda` subpath. Real `LambdaClient`
13
+ instances can be routed into sim Lambda with
14
+ [SDK interception](https://yulinsim.dev/sdk/ "Simulated AWS SDK interception docs").
15
+
16
+ ## Creating and invoking a function
17
+
18
+ The quickest way to a working function is to pass a real in-process handler function through the
19
+ SDK-shaped `Code.ZipFile` input with `makeLambdaZipFileInput(...)`. The handler is an ordinary
20
+ function in your Node.js process. It can be stepped through in a debugger and can close over local
21
+ state.
22
+
23
+ ```typescript sim-lambda-create-and-invoke
24
+ /**
25
+ * Creating and invoking a simulated Lambda function backed by a real
26
+ * in-process handler function.
27
+ */
28
+
29
+ import {
30
+ CreateFunctionCommand,
31
+ GetFunctionCommand,
32
+ InvokeCommand,
33
+ } from "@aws-sdk/client-lambda";
34
+
35
+ import { SimAws } from "@kensio/yulin";
36
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
37
+
38
+ const simAws = new SimAws();
39
+ const lambda = simAws.lambda();
40
+
41
+ await lambda.createFunction(
42
+ new CreateFunctionCommand({
43
+ FunctionName: "greeter",
44
+ Role: "arn:aws:iam::111111111111:role/GreeterRole",
45
+ Code: {
46
+ ZipFile: makeLambdaZipFileInput(
47
+ (event: { name: string }) => `Hello ${event.name}`,
48
+ ),
49
+ },
50
+ }),
51
+ );
52
+
53
+ const invokeOutput = await lambda.invoke(
54
+ new InvokeCommand({
55
+ FunctionName: "greeter",
56
+ Payload: JSON.stringify({ name: "Yulin" }),
57
+ }),
58
+ );
59
+
60
+ if (invokeOutput.Payload === undefined) throw new Error("No invoke Payload");
61
+ console.log(invokeOutput.StatusCode);
62
+ console.log(Buffer.from(invokeOutput.Payload).toString());
63
+
64
+ await simAws.backgroundTasksComplete();
65
+
66
+ const fetched = await lambda.getFunction(
67
+ new GetFunctionCommand({ FunctionName: "greeter" }),
68
+ );
69
+ console.log(fetched.Configuration.State);
70
+ ```
71
+
72
+ Creating a function requires an execution `Role` ARN, as on real AWS. A new function starts in the
73
+ `Pending` state and becomes `Active` in the background. Wait with
74
+ `simAws.backgroundTasksComplete()` when a test asserts on the `Active` state.
75
+
76
+ Handlers use the same signature as real Node.js Lambda handlers, `(event, context, callback)`. All
77
+ the real completion styles work. Return a promise, return a plain value, call the callback, or use
78
+ the legacy context `done`/`fail`/`succeed` methods. Typed handlers written against the `aws-lambda`
79
+ typings package can be passed in unchanged.
80
+
81
+ A handler that throws is reported AWS-style. The invocation output has `FunctionError:
82
+ "Unhandled"` and the payload is an error document with `errorType`, `errorMessage`, and `trace`.
83
+ The invoke call itself returns normally.
84
+
85
+ ## Zip-packaged code and the vm runtime
86
+
87
+ A real in-process handler is the one to reach for first. `makeLambdaZipFileInput(...)` backs a
88
+ function created through the SDK with one, and an [executable binding](#executable-bindings) backs
89
+ one a template declares. The handler stops on a breakpoint and can close over the test's own state.
90
+ Run the packaged code where the test is about the bundle a deployment ships, its imports and its
91
+ bundling included. Both paths invoke through simulated Lambda, and simulated IAM authorizes the
92
+ handler's own AWS calls under the execution role either way.
93
+
94
+ Real Lambda receives function code as a zip archive. `makeLambdaCodeZip(...)` builds real zip
95
+ bytes from a source string (which becomes a single `index.js` module) or from a files map keyed by
96
+ archive path (like a bundled deployment package). The archive runs in a Node.js `vm` context with
97
+ real cold-start semantics. The module is imported once, on first invocation, and module state stays
98
+ warm across invocations.
99
+
100
+ ```typescript sim-lambda-zip-code-vm-runtime
101
+ /**
102
+ * Running zip-packaged function code in the simulated vm runtime.
103
+ */
104
+
105
+ import { CreateFunctionCommand, InvokeCommand } from "@aws-sdk/client-lambda";
106
+
107
+ import { SimAws } from "@kensio/yulin";
108
+ import { makeLambdaCodeZip } from "@kensio/yulin/lambda";
109
+
110
+ const simAws = new SimAws();
111
+ const lambda = simAws.lambda();
112
+
113
+ const codeZip = makeLambdaCodeZip(`
114
+ let invocations = 0;
115
+ exports.handler = async (event) => {
116
+ invocations += 1;
117
+ return { name: event.name, invocations };
118
+ };
119
+ `);
120
+
121
+ await lambda.createFunction(
122
+ new CreateFunctionCommand({
123
+ FunctionName: "warm-counter",
124
+ Role: "arn:aws:iam::111111111111:role/WarmCounterRole",
125
+ Handler: "index.handler",
126
+ Runtime: "nodejs22.x",
127
+ Code: { ZipFile: codeZip },
128
+ }),
129
+ );
130
+
131
+ const first = await lambda.invoke(
132
+ new InvokeCommand({
133
+ FunctionName: "warm-counter",
134
+ Payload: JSON.stringify({ name: "one" }),
135
+ }),
136
+ );
137
+ const second = await lambda.invoke(
138
+ new InvokeCommand({
139
+ FunctionName: "warm-counter",
140
+ Payload: JSON.stringify({ name: "two" }),
141
+ }),
142
+ );
143
+
144
+ if (first.Payload === undefined || second.Payload === undefined) {
145
+ throw new Error("No invoke Payload");
146
+ }
147
+ console.log(Buffer.from(first.Payload).toString());
148
+ console.log(Buffer.from(second.Payload).toString());
149
+
150
+ await simAws.backgroundTasksComplete();
151
+ ```
152
+
153
+ The vm runtime models the real Node.js runtime closely:
154
+
155
+ - The `Handler` string selects the module and export, e.g. `index.handler` or `src/app.handler`.
156
+ - Modules in the archive can `require` each other with relative paths, use Node.js built-in
157
+ modules, and use dependencies bundled under the archive's `node_modules/`.
158
+ - The sandbox provides an AWS-like `process.env` with the standard runtime variables
159
+ (`AWS_REGION`, `AWS_LAMBDA_FUNCTION_NAME`, `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, ...).
160
+ - The sandbox provides writable `process.stdout` and `process.stderr`, with its `console` built
161
+ over them, as the real runtime does. See [What a handler prints](#what-a-handler-prints).
162
+ - Import and handler problems surface as invocation errors, with the real runtime error types
163
+ (`Runtime.ImportModuleError`, `Runtime.HandlerNotFound`, `Runtime.UserCodeSyntaxError`,
164
+ `Runtime.MalformedHandlerName`). Creation succeeds either way.
165
+
166
+ Code is CommonJS, as zipped `.js` files are on the real `nodejs` runtimes. An ES module deployment
167
+ package is refused at cold start. A handler file ending in `.mjs` is reported as
168
+ `Runtime.ImportModuleError` naming the file. A `.js` file opening on `import` is reported as
169
+ `Runtime.UserCodeSyntaxError: Cannot use import statement outside a module`. The archive's root
170
+ `package.json` goes unread. Declaring `"type": "module"` in it makes no difference to either.
171
+ Real Lambda has run ES module packages since nodejs14.x, and `NodejsFunction` in CDK emits one
172
+ under `format: OutputFormat.ESM`.
173
+
174
+ There are two ways round that. An [executable binding](#executable-bindings) or
175
+ `makeLambdaZipFileInput(...)` backs the function with a real in-process handler. The test's own
176
+ module system loads that handler, and its format is whatever the test file's is. An ESM project
177
+ needs nothing further. Where the archive itself has to be the deployed artefact, compile a CommonJS
178
+ build of the same source and deploy that as the function's code.
179
+
180
+ `Code.ZipFile` bytes that fail to unzip are rejected at creation with the AWS-like
181
+ `InvalidParameterValueException: Could not unzip uploaded file`.
182
+
183
+ The archives are real zip files, and they interoperate with real tooling in both directions. A zip
184
+ built by any other tool works as `Code.ZipFile` input, and `makeLambdaCodeZip` output can be
185
+ unzipped normally.
186
+
187
+ ### What a handler prints
188
+
189
+ The sandbox has writable standard streams. `process.stdout.write(...)` and
190
+ `process.stderr.write(...)` work inside a handler, and its `console` is built over them as the real
191
+ runtime's is. A logging library that builds its own console over them works too:
192
+
193
+ ```javascript
194
+ const { Console } = require("node:console");
195
+ const logger = new Console({ stdout: process.stdout, stderr: process.stderr });
196
+ ```
197
+
198
+ That is what AWS Lambda Powertools' `Logger` does, at module scope, and a bundled Powertools handler
199
+ runs here. Its `Metrics` writes its embedded metric format document to standard output, where the
200
+ metrics a handler emitted can be read back the same way its log lines can.
201
+
202
+ What a handler prints is recorded into the function's log group in
203
+ [simulated CloudWatch Logs](https://yulinsim.dev/services/logs/ "Simulated CloudWatch Logs usage docs"), at
204
+ `/aws/lambda/<function name>`. A test asserts on it by searching that group:
205
+
206
+ ```typescript
207
+ const found = await simAws.logs().filterLogEvents(
208
+ new FilterLogEventsCommand({
209
+ logGroupName: "/aws/lambda/orders",
210
+ filterPattern: "ERROR",
211
+ }),
212
+ );
213
+ ```
214
+
215
+ One line becomes one log event, as it does in an account. A handler printing a multi-line object
216
+ gets several events, and a search for one of those lines finds it. EMF metric documents go to the
217
+ same place, since Powertools' `Metrics` writes them to standard output.
218
+
219
+ Each invocation also reaches the matching host stream, standard output to standard output and
220
+ standard error to standard error, where a test or a `pnpm run dev` session already sees output. That
221
+ is a tee rather than a redirect. Real Lambda sends output to CloudWatch Logs and nowhere else, and a
222
+ test tool that swallowed the output would make a failing test harder to debug than it is with none
223
+ of this.
224
+
225
+ `context.logGroupName` and `context.logStreamName` name the group and stream that were actually
226
+ written to, and stream names use the real `YYYY/MM/DD/[$LATEST]<hash>` format. The hash identifies
227
+ the execution environment rather than the request. Match the shape, and leave the value alone.
228
+
229
+ A function backed by a handler function reference is recorded too, whether the handler arrived
230
+ through `Code.ZipFile`, through a CloudFormation stack binding, or as the image of a simulated ECR
231
+ repository. Such a handler is a closure over your own module scope and prints through the same
232
+ console and standard streams as the rest of the test run, and both are bridged to the function's log
233
+ group for the length of an invocation, in the way `process.env` and `Date` are. `console.log`,
234
+ `console.info`, `console.debug`, `console.warn` and `console.error` all arrive, along with anything
235
+ written to `process.stdout` or `process.stderr`, so a logging library building its own console over
236
+ the process streams is recorded as well. Printing with no invocation running reaches the host
237
+ console alone.
238
+
239
+ A test that binds its handler for a breakpoint, or so the handler can close over its own state, can
240
+ still assert on the lines it logged:
241
+
242
+ ```typescript sim-lambda-bound-handler-output
243
+ /**
244
+ * Asserting on what a bound in-process Lambda handler printed.
245
+ */
246
+
247
+ import { FilterLogEventsCommand } from "@aws-sdk/client-cloudwatch-logs";
248
+ import { CreateFunctionCommand, InvokeCommand } from "@aws-sdk/client-lambda";
249
+
250
+ import { SimAws } from "@kensio/yulin";
251
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
252
+
253
+ const simAws = new SimAws();
254
+
255
+ await simAws.lambda().createFunction(
256
+ new CreateFunctionCommand({
257
+ FunctionName: "orders",
258
+ Role: "arn:aws:iam::111111111111:role/OrdersRole",
259
+ Code: {
260
+ ZipFile: makeLambdaZipFileInput((event: { orderId: string }) => {
261
+ console.log(`ERROR ${event.orderId} has no items`);
262
+
263
+ return "rejected";
264
+ }),
265
+ },
266
+ }),
267
+ );
268
+
269
+ await simAws.lambda().invoke(
270
+ new InvokeCommand({
271
+ FunctionName: "orders",
272
+ Payload: JSON.stringify({ orderId: "order-1" }),
273
+ }),
274
+ );
275
+
276
+ const logged = await simAws.logs().filterLogEvents(
277
+ new FilterLogEventsCommand({
278
+ logGroupName: "/aws/lambda/orders",
279
+ filterPattern: "ERROR",
280
+ }),
281
+ );
282
+
283
+ // [ 'ERROR order-1 has no items' ]
284
+ console.log(logged.events?.map((event) => event.message));
285
+
286
+ await simAws.backgroundTasksComplete();
287
+ ```
288
+
289
+ ### When a handler throws
290
+
291
+ An invocation that ends in an error nothing caught leaves an `ERROR Invoke Error` line in the
292
+ function's log group, as it does in an account. The line carries a JSON document holding the error's
293
+ type, its message and its stack, with one array element per stack frame. Whatever the handler
294
+ printed before it failed is recorded above it. A handler bound in-process and one packaged as zip
295
+ code are both recorded this way.
296
+
297
+ ```typescript sim-lambda-unhandled-error-output
298
+ /**
299
+ * Reading why an invocation failed out of the function's log group.
300
+ */
301
+
302
+ import { FilterLogEventsCommand } from "@aws-sdk/client-cloudwatch-logs";
303
+ import { CreateFunctionCommand, InvokeCommand } from "@aws-sdk/client-lambda";
304
+
305
+ import { SimAws } from "@kensio/yulin";
306
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
307
+
308
+ const simAws = new SimAws();
309
+
310
+ await simAws.lambda().createFunction(
311
+ new CreateFunctionCommand({
312
+ FunctionName: "orders",
313
+ Role: "arn:aws:iam::111111111111:role/OrdersRole",
314
+ Code: {
315
+ ZipFile: makeLambdaZipFileInput(() => {
316
+ console.log("INFO handling order-1");
317
+
318
+ throw new Error("order has no items");
319
+ }),
320
+ },
321
+ }),
322
+ );
323
+
324
+ await simAws.lambda().invoke(new InvokeCommand({ FunctionName: "orders" }));
325
+
326
+ const failure = await simAws.logs().filterLogEvents(
327
+ new FilterLogEventsCommand({
328
+ logGroupName: "/aws/lambda/orders",
329
+ filterPattern: '"Invoke Error"',
330
+ }),
331
+ );
332
+
333
+ // ERROR Invoke Error {"errorType":"Error","errorMessage":"order has no items",...}
334
+ console.log(failure.events?.at(0)?.message);
335
+
336
+ await simAws.backgroundTasksComplete();
337
+ ```
338
+
339
+ The caller still hears about it too. `Invoke` answers with `FunctionError` set to `Unhandled` and
340
+ the same error document in its response payload.
341
+
342
+ Real Lambda writes `START`, `END` and `REPORT` lines around every invocation. Simulated Lambda
343
+ writes none of those.
344
+
345
+ ## Function code from S3
346
+
347
+ Function code can also be fetched from a zip object stored in sim S3, as SAM and CDK deployments
348
+ do on real AWS. The code object is fetched once at creation time, as the creating caller, so
349
+ simulated IAM applies to the code object read.
350
+
351
+ ```typescript sim-lambda-s3-code
352
+ /**
353
+ * Creating a simulated Lambda function from a code zip stored in sim S3.
354
+ */
355
+
356
+ import { CreateFunctionCommand, InvokeCommand } from "@aws-sdk/client-lambda";
357
+ import { CreateBucketCommand, PutObjectCommand } from "@aws-sdk/client-s3";
358
+
359
+ import { SimAws } from "@kensio/yulin";
360
+ import { makeLambdaCodeZip } from "@kensio/yulin/lambda";
361
+
362
+ const simAws = new SimAws();
363
+
364
+ await simAws
365
+ .s3()
366
+ .createBucket(new CreateBucketCommand({ Bucket: "code-bucket" }));
367
+ await simAws.s3().putObject(
368
+ new PutObjectCommand({
369
+ Bucket: "code-bucket",
370
+ Key: "artifacts/greeter.zip",
371
+ Body: makeLambdaCodeZip(
372
+ "exports.handler = async (event) => 'Hello ' + event.name + ' from S3';",
373
+ ),
374
+ }),
375
+ );
376
+
377
+ await simAws.lambda().createFunction(
378
+ new CreateFunctionCommand({
379
+ FunctionName: "s3-greeter",
380
+ Role: "arn:aws:iam::111111111111:role/S3GreeterRole",
381
+ Handler: "index.handler",
382
+ Code: {
383
+ S3Bucket: "code-bucket",
384
+ S3Key: "artifacts/greeter.zip",
385
+ },
386
+ }),
387
+ );
388
+
389
+ const output = await simAws.lambda().invoke(
390
+ new InvokeCommand({
391
+ FunctionName: "s3-greeter",
392
+ Payload: JSON.stringify({ name: "Yulin" }),
393
+ }),
394
+ );
395
+
396
+ if (output.Payload === undefined) throw new Error("No invoke Payload");
397
+ console.log(Buffer.from(output.Payload).toString());
398
+
399
+ await simAws.backgroundTasksComplete();
400
+ ```
401
+
402
+ S3 lookup failures are wrapped AWS-style, e.g. `Error occurred while GetObject. S3 Error Code:
403
+ NoSuchKey. ...`. `S3ObjectVersion` is accepted but ignored, as sim S3 has no object versioning
404
+ yet. A standalone `SimLambda` (constructed directly, outside `SimAws`) has no sim S3 to
405
+ fetch from. `SimAws`-created Lambda wires the same-scope sim S3 automatically, matching real
406
+ Lambda's requirement for a same-region code bucket.
407
+
408
+ ## Replacing a function's code
409
+
410
+ `UpdateFunctionCodeCommand` replaces the code `$LATEST` runs, taking the same four code shapes `CreateFunction` takes. It carries them at the top level of its input rather than under `Code`, as real Lambda does. A test that redeploys part-way through, or that wants a function to start failing after a few invocations, changes what runs without deleting the function.
411
+
412
+ The function keeps everything else it holds. Its name, ARN, execution Role, environment variables, timeout, memory, [resource-based policy](#resource-based-policies), [Function URL](#function-urls), published versions and aliases all survive, which is what separates this from deleting the function and creating it again.
413
+
414
+ ```typescript sim-lambda-update-function-code
415
+ /**
416
+ * Replacing the code a simulated Lambda function runs, part-way through.
417
+ */
418
+
419
+ import {
420
+ CreateFunctionCommand,
421
+ InvokeCommand,
422
+ UpdateFunctionCodeCommand,
423
+ } from "@aws-sdk/client-lambda";
424
+
425
+ import { SimAws } from "@kensio/yulin";
426
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
427
+
428
+ const simAws = new SimAws();
429
+ const lambda = simAws.lambda();
430
+
431
+ await lambda.createFunction(
432
+ new CreateFunctionCommand({
433
+ FunctionName: "orders",
434
+ Role: "arn:aws:iam::111111111111:role/OrdersRole",
435
+ Code: { ZipFile: makeLambdaZipFileInput(() => ({ ok: true })) },
436
+ }),
437
+ );
438
+
439
+ await lambda.updateFunctionCode(
440
+ new UpdateFunctionCodeCommand({
441
+ FunctionName: "orders",
442
+ ZipFile: makeLambdaZipFileInput(() => {
443
+ throw new Error("the order service is down");
444
+ }),
445
+ }),
446
+ );
447
+
448
+ const invoked = await lambda.invoke(
449
+ new InvokeCommand({ FunctionName: "orders" }),
450
+ );
451
+ console.log(invoked.FunctionError);
452
+
453
+ await simAws.backgroundTasksComplete();
454
+ ```
455
+
456
+ Zip code runs under the `Handler` the function already has, because `UpdateFunctionCode` carries none of its own. Replacing a handler-reference function's code with a zip archive is refused for that reason. Set a `Handler` with [`UpdateFunctionConfiguration`](#changing-a-functions-settings) first, then replace the code.
457
+
458
+ A published version keeps the code it was published with, so a version published before an update goes on running it while `$LATEST` runs the replacement. `Publish: true` publishes a version of the replacement code and answers with that version rather than with `$LATEST`. See [Versions and aliases](#versions-and-aliases).
459
+
460
+ ## Changing a function's settings
461
+
462
+ `UpdateFunctionConfigurationCommand` changes the settings simulated Lambda models: `Role`, `Handler`, `Runtime`, `Description`, `Timeout`, `MemorySize` and `Environment`. A member the request leaves out keeps the value the function has. Changing one setting says nothing about the rest.
463
+
464
+ `Environment` replaces the whole variable map rather than merging into it, as on real AWS. A variable the request leaves out is gone, and `Variables: {}` clears them all. The names are validated the way `CreateFunction` validates them, down to the reserved ones the runtime owns.
465
+
466
+ ```typescript sim-lambda-update-function-configuration
467
+ /**
468
+ * Changing a simulated Lambda function's timeout and environment variables.
469
+ */
470
+
471
+ import {
472
+ CreateFunctionCommand,
473
+ InvokeCommand,
474
+ UpdateFunctionConfigurationCommand,
475
+ } from "@aws-sdk/client-lambda";
476
+
477
+ import { SimAws } from "@kensio/yulin";
478
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
479
+
480
+ const simAws = new SimAws();
481
+ const lambda = simAws.lambda();
482
+
483
+ await lambda.createFunction(
484
+ new CreateFunctionCommand({
485
+ FunctionName: "orders",
486
+ Role: "arn:aws:iam::111111111111:role/OrdersRole",
487
+ Timeout: 30,
488
+ Environment: { Variables: { ORDERS_TABLE: "orders-v1" } },
489
+ Code: {
490
+ ZipFile: makeLambdaZipFileInput((_event, context) => ({
491
+ table: process.env["ORDERS_TABLE"],
492
+ remainingMs: context.getRemainingTimeInMillis(),
493
+ })),
494
+ },
495
+ }),
496
+ );
497
+
498
+ await lambda.updateFunctionConfiguration(
499
+ new UpdateFunctionConfigurationCommand({
500
+ FunctionName: "orders",
501
+ Timeout: 1,
502
+ Environment: { Variables: { ORDERS_TABLE: "orders-v2" } },
503
+ }),
504
+ );
505
+
506
+ const invoked = await lambda.invoke(
507
+ new InvokeCommand({ FunctionName: "orders" }),
508
+ );
509
+ console.log(Buffer.from(invoked.Payload ?? new Uint8Array()).toString());
510
+
511
+ await simAws.backgroundTasksComplete();
512
+ ```
513
+
514
+ The function keeps its name, ARN, code, [resource-based policy](#resource-based-policies), [Function URL](#function-urls), published versions and aliases. A version published beforehand keeps the settings it was published with, and one published afterwards carries the changed ones. See [Versions and aliases](#versions-and-aliases).
515
+
516
+ A changed `Environment`, `MemorySize` or `Handler` reaches the code itself. The next invocation of a [zip code function](#zip-packaged-code-and-the-vm-runtime) cold starts under the new settings. Whatever its module had in memory goes, the way it goes on real Lambda when the configuration changes. A `Handler` change picks a different export out of the archive already deployed, with no need to upload it again.
517
+
518
+ ## Listing the functions
519
+
520
+ `ListFunctionsCommand` reports every function in the Account and Region, each described as `GetFunction` describes it. `FunctionVersion: "ALL"` adds each function's published versions to the listing.
521
+
522
+ ```typescript sim-lambda-list-functions
523
+ /**
524
+ * Listing the simulated Lambda functions an Account and Region holds.
525
+ */
526
+
527
+ import {
528
+ CreateFunctionCommand,
529
+ ListFunctionsCommand,
530
+ } from "@aws-sdk/client-lambda";
531
+
532
+ import { SimAws } from "@kensio/yulin";
533
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
534
+
535
+ const simAws = new SimAws();
536
+ const lambda = simAws.lambda();
537
+
538
+ for (const functionName of ["orders", "invoices"]) {
539
+ await lambda.createFunction(
540
+ new CreateFunctionCommand({
541
+ FunctionName: functionName,
542
+ Role: "arn:aws:iam::111111111111:role/OrdersRole",
543
+ Code: { ZipFile: makeLambdaZipFileInput(() => functionName) },
544
+ }),
545
+ );
546
+ }
547
+
548
+ const listed = await lambda.listFunctions(new ListFunctionsCommand({}));
549
+ console.log(listed.Functions.map((simFunction) => simFunction.FunctionName));
550
+
551
+ await simAws.backgroundTasksComplete();
552
+ ```
553
+
554
+ The listing is authorized as a whole, against `lambda:ListFunctions` on `*`, the way AWS documents the permission. A caller without it is denied rather than given a filtered list.
555
+
556
+ ## The runtime-provided AWS SDK
557
+
558
+ Like the real Lambda Node.js runtime, the simulated runtime provides AWS SDK v3 packages without
559
+ them being bundled in the code archive. `require("@aws-sdk/client-s3")`, or any other `@aws-sdk/*`
560
+ package installed in the host project, resolves to the real package with its clients routed into
561
+ the owning simulated AWS environment. Calls the function code makes run as the function's
562
+ execution role, and simulated IAM authorizes them just like real Lambda execution roles.
563
+
564
+ ```typescript sim-lambda-runtime-provided-sdk
565
+ /**
566
+ * Simulated Lambda function code reading sim S3 through the
567
+ * runtime-provided AWS SDK, authorized as its execution role.
568
+ */
569
+
570
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
571
+ import { CreateFunctionCommand, InvokeCommand } from "@aws-sdk/client-lambda";
572
+ import { CreateBucketCommand, PutObjectCommand } from "@aws-sdk/client-s3";
573
+
574
+ import { SimAws } from "@kensio/yulin";
575
+ import { makeLambdaCodeZip } from "@kensio/yulin/lambda";
576
+
577
+ const simAws = new SimAws();
578
+
579
+ // An object for the function to read.
580
+ await simAws
581
+ .s3()
582
+ .createBucket(new CreateBucketCommand({ Bucket: "data-bucket" }));
583
+ await simAws.s3().putObject(
584
+ new PutObjectCommand({
585
+ Bucket: "data-bucket",
586
+ Key: "greeting.txt",
587
+ Body: "Hello from S3",
588
+ }),
589
+ );
590
+
591
+ // An execution role allowed to read it.
592
+ const roleCreation = await simAws.iam().createRole(
593
+ new CreateRoleCommand({
594
+ RoleName: "ReaderRole",
595
+ AssumeRolePolicyDocument: JSON.stringify({
596
+ Version: "2012-10-17",
597
+ Statement: {
598
+ Effect: "Allow",
599
+ Principal: { Service: "lambda.amazonaws.com" },
600
+ Action: "sts:AssumeRole",
601
+ },
602
+ }),
603
+ }),
604
+ );
605
+ await simAws.iam().putRolePolicy(
606
+ new PutRolePolicyCommand({
607
+ RoleName: "ReaderRole",
608
+ PolicyName: "ReadDataBucket",
609
+ PolicyDocument: JSON.stringify({
610
+ Version: "2012-10-17",
611
+ Statement: {
612
+ Effect: "Allow",
613
+ Action: "s3:GetObject",
614
+ Resource: "arn:aws:s3:::data-bucket/*",
615
+ },
616
+ }),
617
+ }),
618
+ );
619
+
620
+ // Function code using the runtime-provided AWS SDK.
621
+ await simAws.lambda().createFunction(
622
+ new CreateFunctionCommand({
623
+ FunctionName: "reader",
624
+ Role: roleCreation.Role.Arn,
625
+ Handler: "index.handler",
626
+ Code: {
627
+ ZipFile: makeLambdaCodeZip(`
628
+ const { S3Client, GetObjectCommand } = require("@aws-sdk/client-s3");
629
+ const s3Client = new S3Client({});
630
+ exports.handler = async (event) => {
631
+ const output = await s3Client.send(
632
+ new GetObjectCommand({
633
+ Bucket: "data-bucket",
634
+ Key: event.objectKey,
635
+ }),
636
+ );
637
+ return await output.Body.transformToString();
638
+ };
639
+ `),
640
+ },
641
+ }),
642
+ );
643
+
644
+ const output = await simAws.lambda().invoke(
645
+ new InvokeCommand({
646
+ FunctionName: "reader",
647
+ Payload: JSON.stringify({ objectKey: "greeting.txt" }),
648
+ }),
649
+ );
650
+
651
+ if (output.Payload === undefined) throw new Error("No invoke Payload");
652
+ console.log(Buffer.from(output.Payload).toString());
653
+
654
+ await simAws.backgroundTasksComplete();
655
+ ```
656
+
657
+ If the execution role lacks permission for a call the handler makes, simulated IAM denies it and
658
+ the invocation reports the denial as an unhandled function error, just as a real execution-role
659
+ denial surfaces inside the handler.
660
+
661
+ A client constructed without a region defaults to the function's account and region scope, as the
662
+ real runtime's `AWS_REGION` provides. An explicit region on the client wins. The archive always
663
+ takes precedence. A package bundled under the archive's `node_modules/` is used as it stands, and
664
+ reaches the simulation through the transport below.
665
+
666
+ An `@aws-sdk/*` package the project has not installed is refused when the function code requires
667
+ it. The message names every other AWS SDK package the code imports and the project is missing too.
668
+ One install then covers the set. (The archive is the only place to read what a third-party function
669
+ imports.) Packages bundled under the archive's `node_modules/` are left out of that list, along with
670
+ anything only a bundled dependency imports.
671
+
672
+ ### Function code that bundles the SDK
673
+
674
+ Some deployment packages inline the SDK rather than relying on the runtime to provide it. CDK's
675
+ `NodejsFunction` does that when given `bundling: { externalModules: [] }`. There is then no
676
+ `@aws-sdk/*` module left for the runtime to provide, and nothing to intercept.
677
+
678
+ Those functions reach the simulation anyway. The runtime provides their HTTP transport, `node:http`
679
+ and `node:https`, and a request addressed to an AWS API endpoint is answered from the same
680
+ simulated services, as the same execution role, instead of going to the network. The execution
681
+ role credentials are in the function's environment, as real Lambda puts them there. The SDK resolves
682
+ credentials inside the sandbox, where it would otherwise fail with `CredentialsProviderError`.
683
+
684
+ A serialized request states which operation it is and carries its input for the services using the
685
+ AWS JSON protocol. Those are the ones that can be read back without the operation's schema:
686
+
687
+ ACM, CloudWatch Logs, Cognito Identity Provider, DynamoDB, DynamoDB Streams, ECS, EventBridge, KMS,
688
+ Rekognition, Secrets Manager, SQS and SSM.
689
+
690
+ A bundled call to any other simulated service, such as S3, SNS, SES or Lambda itself, fails with an
691
+ error naming the service. Leaving the SDK out of that archive puts it back
692
+ on the module interception path above, which every simulated service is reachable through.
693
+
694
+ Values the JSON protocols encode travel in their encoded form. A binary attribute written through a
695
+ bundled SDK is stored base64-encoded and decodes correctly when the same path reads it back, where
696
+ an in-process intercepted client reading it sees the encoded string.
697
+
698
+ Only service API endpoints are answered as Commands. The endpoint of one resource, such as a Lambda
699
+ Function URL or an API Gateway HTTP API, carries an ordinary HTTP request. Those are routed by
700
+ hostname, along with everything else a handler asks for over HTTP.
701
+
702
+ ## The HTTP requests function code makes
703
+
704
+ A request from a handler to a hostname the simulation serves is answered by the simulation. It
705
+ never leaves the process. `fetch`, `node:http` and `node:https` all reach it, so it makes no
706
+ difference which client the handler was written with.
707
+
708
+ Two kinds of hostname are served. Everything simulated Route53 resolves is answered over the same
709
+ in-process HTTP entry point a browser on localhost reaches. That covers a
710
+ [Cognito user pool domain](https://yulinsim.dev/services/cognito/ "Simulated Cognito usage docs") in both of its forms
711
+ (`<prefix>.auth.<region>.amazoncognito.com` and a custom domain such as `auth.example.com`), an
712
+ [API Gateway HTTP API](https://yulinsim.dev/services/apigatewayv2/ "Simulated API Gateway usage docs"), a
713
+ [load balancer](https://yulinsim.dev/services/elbv2/ "Simulated ELBv2 usage docs") and anything a hosted-zone record points at
714
+ one of those.
715
+
716
+ The AWS service API endpoints are the other kind, and a request to one is read for what it carries.
717
+ A serialized Command goes to the simulated operation it names (the section above). A request
718
+ carrying no operation header and no signature is an ordinary HTTP request, and the endpoint serves
719
+ it the way the local hostname does. That is how a pool's JWKS is read at
720
+ `cognito-idp.<region>.amazonaws.com` by a client holding no credentials.
721
+
722
+ A hostname the simulation serves nothing at goes where it was addressed. A handler calling a
723
+ payment API or a webhook reaches the network as it always did.
724
+
725
+ The authorization code grant is the request this exists for. Cognito issues an authorization code
726
+ to the browser and the application exchanges it for tokens from its own server, and there is no SDK
727
+ operation for that exchange. It is a POST to `/oauth2/token` on the pool's domain, which is a
728
+ hostname the simulation now serves:
729
+
730
+ ```typescript sim-lambda-outbound-token-exchange
731
+ /**
732
+ * A simulated Lambda exchanging an authorization code for tokens at the
733
+ * simulated Cognito user pool domain that issued it.
734
+ */
735
+
736
+ import { CreateFunctionCommand, InvokeCommand } from "@aws-sdk/client-lambda";
737
+
738
+ import { SimAws } from "@kensio/yulin";
739
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
740
+
741
+ const simAws = new SimAws({ defaultRegionName: "eu-west-2" });
742
+
743
+ // The pool's domain, the app client the callback authenticates as, and the
744
+ // code the browser carried to it.
745
+ declare const domainHost: string;
746
+ declare const clientId: string;
747
+ declare const clientSecret: string;
748
+ declare const callbackUrl: string;
749
+ declare const authorizationCode: string;
750
+
751
+ await simAws.lambda().createFunction(
752
+ new CreateFunctionCommand({
753
+ FunctionName: "user",
754
+ Role: "arn:aws:iam::111111111111:role/UserRole",
755
+ Code: {
756
+ ZipFile: makeLambdaZipFileInput(async (event: { code: string }) => {
757
+ const credentials = `${clientId}:${clientSecret}`;
758
+ const response = await fetch(`https://${domainHost}/oauth2/token`, {
759
+ method: "POST",
760
+ headers: {
761
+ "content-type": "application/x-www-form-urlencoded",
762
+ authorization: `Basic ${Buffer.from(credentials).toString("base64")}`,
763
+ },
764
+ body: new URLSearchParams({
765
+ grant_type: "authorization_code",
766
+ code: event.code,
767
+ redirect_uri: callbackUrl,
768
+ }).toString(),
769
+ });
770
+
771
+ return await response.json();
772
+ }),
773
+ },
774
+ }),
775
+ );
776
+
777
+ const output = await simAws.lambda().invoke(
778
+ new InvokeCommand({
779
+ FunctionName: "user",
780
+ Payload: JSON.stringify({ code: authorizationCode }),
781
+ }),
782
+ );
783
+
784
+ if (output.Payload === undefined) throw new Error("No invoke Payload");
785
+
786
+ // The id, access and refresh tokens the pool issued.
787
+ console.log(Buffer.from(output.Payload).toString());
788
+
789
+ await simAws.backgroundTasksComplete();
790
+ ```
791
+
792
+ Zip-packaged code takes its three clients from the sandbox the vm runtime builds around it, and
793
+ nothing outside that sandbox is touched. A handler function reference is a closure over your own
794
+ module scope and reads the same globals as the rest of the test run, so those globals are bridged
795
+ for the length of an invocation, in the way `process.env` and `Date` are (see
796
+ [Read environment variables inside the handler](#read-environment-variables-inside-the-handler)).
797
+ With no invocation running they behave as they always did, which leaves a request a handler module
798
+ makes while it is being imported going to the network.
799
+
800
+ A response comes back from the simulation as it was answered. A redirect arrives as the `302` it
801
+ is, where the host `fetch` would have followed it.
802
+
803
+ ### Verifying a Cognito token in a handler
804
+
805
+ An API that takes a bearer token verifies it against the keys the pool publishes.
806
+ `CognitoJwtVerifier` from `aws-jwt-verify` builds the JWKS URL from the pool id
807
+ (`https://cognito-idp.<region>.amazonaws.com/<userPoolId>/.well-known/jwks.json`) and accepts no
808
+ other, and fetches it with `node:https` the first time it verifies anything. Both of those are the
809
+ simulation's to answer inside a handler, so the verifier in the deployed code verifies a simulated
810
+ pool's token with no cache primed and no setup around it:
811
+
812
+ ```typescript sim-lambda-verify-cognito-token
813
+ /**
814
+ * A simulated Lambda verifying a Cognito access token, with a verifier that
815
+ * goes and fetches the pool's JWKS for itself.
816
+ */
817
+
818
+ import { CreateFunctionCommand, InvokeCommand } from "@aws-sdk/client-lambda";
819
+ import { CognitoJwtVerifier } from "aws-jwt-verify";
820
+
821
+ import { SimAws } from "@kensio/yulin";
822
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
823
+
824
+ const simAws = new SimAws({ defaultRegionName: "eu-west-2" });
825
+
826
+ // The pool the API trusts, the app client its tokens are issued to, and the
827
+ // token a caller presented.
828
+ declare const userPoolId: string;
829
+ declare const clientId: string;
830
+ declare const accessToken: string;
831
+
832
+ await simAws.lambda().createFunction(
833
+ new CreateFunctionCommand({
834
+ FunctionName: "api",
835
+ Role: "arn:aws:iam::111111111111:role/ApiRole",
836
+ Code: {
837
+ ZipFile: makeLambdaZipFileInput(async (event: { token: string }) => {
838
+ const verifier = CognitoJwtVerifier.create({
839
+ userPoolId,
840
+ tokenUse: "access",
841
+ clientId,
842
+ });
843
+
844
+ return await verifier.verify(event.token);
845
+ }),
846
+ },
847
+ }),
848
+ );
849
+
850
+ const output = await simAws.lambda().invoke(
851
+ new InvokeCommand({
852
+ FunctionName: "api",
853
+ Payload: JSON.stringify({ token: accessToken }),
854
+ }),
855
+ );
856
+
857
+ if (output.Payload === undefined) throw new Error("No invoke Payload");
858
+
859
+ // The claims of the access token, read by the verifier the API ships with.
860
+ console.log(Buffer.from(output.Payload).toString());
861
+
862
+ await simAws.backgroundTasksComplete();
863
+ ```
864
+
865
+ The pool's OpenID configuration is served at the same endpoint, at
866
+ `/<userPoolId>/.well-known/openid-configuration`. Its `issuer` and `jwks_uri` name the local
867
+ hostname the simulation answered on, and a handler that discovered the document can fetch the keys
868
+ it points at. See
869
+ [serving a pool's JWKS](https://yulinsim.dev/services/cognito/#serving-a-pools-jwks-on-localhost "Simulated Cognito usage docs").
870
+
871
+ A Command the same function sends still reaches simulated Cognito. An SDK bundled into the
872
+ deployment package addresses `cognito-idp.<region>.amazonaws.com` as well, and its requests carry
873
+ the operation header that says so.
874
+
875
+ ## Invocation types
876
+
877
+ `InvokeCommand` supports the three AWS invocation types. `RequestResponse` (the default) awaits
878
+ the handler and returns its JSON-serialised result as the response payload. `Event` returns `202`
879
+ immediately and runs the handler in the background. `DryRun` returns `204` without invoking the
880
+ handler at all.
881
+
882
+ ```typescript sim-lambda-invocation-types
883
+ /**
884
+ * Simulated Lambda Event and DryRun invocation types.
885
+ */
886
+
887
+ import { CreateFunctionCommand, InvokeCommand } from "@aws-sdk/client-lambda";
888
+
889
+ import { SimAws } from "@kensio/yulin";
890
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
891
+
892
+ const simAws = new SimAws();
893
+ const lambda = simAws.lambda();
894
+
895
+ const handledEvents: unknown[] = [];
896
+ await lambda.createFunction(
897
+ new CreateFunctionCommand({
898
+ FunctionName: "recorder",
899
+ Role: "arn:aws:iam::111111111111:role/RecorderRole",
900
+ Code: {
901
+ ZipFile: makeLambdaZipFileInput((event) => {
902
+ handledEvents.push(event);
903
+ return null;
904
+ }),
905
+ },
906
+ }),
907
+ );
908
+
909
+ // An Event invocation is accepted before the handler has run.
910
+ const eventOutput = await lambda.invoke(
911
+ new InvokeCommand({
912
+ FunctionName: "recorder",
913
+ InvocationType: "Event",
914
+ Payload: JSON.stringify({ recorded: true }),
915
+ }),
916
+ );
917
+ console.log(eventOutput.StatusCode);
918
+ console.log(handledEvents.length);
919
+
920
+ // The handler runs when simulator background tasks complete.
921
+ await simAws.backgroundTasksComplete();
922
+ console.log(handledEvents.length);
923
+
924
+ // A DryRun invocation never runs the handler.
925
+ const dryRunOutput = await lambda.invoke(
926
+ new InvokeCommand({
927
+ FunctionName: "recorder",
928
+ InvocationType: "DryRun",
929
+ }),
930
+ );
931
+ console.log(dryRunOutput.StatusCode);
932
+ ```
933
+
934
+ `Event` invocation handler errors never reach the caller, who has already been answered. Where the
935
+ handler keeps failing they reach the function's destinations instead, which is what the next section
936
+ covers.
937
+
938
+ ## Asynchronous retries and destinations
939
+
940
+ An `Event` invocation whose handler throws is retried twice, as real Lambda retries one. The retries
941
+ wait on the simulated clock, about a minute before the first and two before the second, so a test
942
+ reaches them by advancing time rather than by waiting for it.
943
+
944
+ `PutFunctionEventInvokeConfigCommand` changes how many retries a function makes and where its
945
+ results go. An invocation that fails its last attempt is sent to the `OnFailure` destination, and
946
+ one whose handler returns is sent to `OnSuccess`. A destination ARN can name a simulated SQS queue,
947
+ SNS topic, EventBridge event bus or Lambda function.
948
+
949
+ ```typescript sim-lambda-async-destinations
950
+ /**
951
+ * Asynchronous invocation retries and an OnFailure destination.
952
+ */
953
+
954
+ import {
955
+ CreateFunctionCommand,
956
+ InvokeCommand,
957
+ PutFunctionEventInvokeConfigCommand,
958
+ } from "@aws-sdk/client-lambda";
959
+ import { CreateQueueCommand, ReceiveMessageCommand } from "@aws-sdk/client-sqs";
960
+
961
+ import { SimAws } from "@kensio/yulin";
962
+ import {
963
+ makeLambdaZipFileInput,
964
+ type SimLambdaDestinationRecord,
965
+ } from "@kensio/yulin/lambda";
966
+
967
+ const simAws = new SimAws();
968
+ const lambda = simAws.lambda();
969
+ const sqs = simAws.sqs();
970
+
971
+ const created = await sqs.createQueue(
972
+ new CreateQueueCommand({ QueueName: "order-failures" }),
973
+ );
974
+
975
+ const attempts: string[] = [];
976
+ await lambda.createFunction(
977
+ new CreateFunctionCommand({
978
+ FunctionName: "orders",
979
+ Role: "arn:aws:iam::111111111111:role/OrdersRole",
980
+ Code: {
981
+ ZipFile: makeLambdaZipFileInput((event: { id: number }) => {
982
+ attempts.push(`tried order ${event.id}`);
983
+ throw new Error("orders handler failed");
984
+ }),
985
+ },
986
+ }),
987
+ );
988
+
989
+ await lambda.putFunctionEventInvokeConfig(
990
+ new PutFunctionEventInvokeConfigCommand({
991
+ FunctionName: "orders",
992
+ MaximumRetryAttempts: 1,
993
+ DestinationConfig: {
994
+ OnFailure: {
995
+ Destination: "arn:aws:sqs:us-east-1:888888888888:order-failures",
996
+ },
997
+ },
998
+ }),
999
+ );
1000
+
1001
+ await lambda.invoke(
1002
+ new InvokeCommand({
1003
+ FunctionName: "orders",
1004
+ InvocationType: "Event",
1005
+ Payload: JSON.stringify({ id: 7 }),
1006
+ }),
1007
+ );
1008
+
1009
+ // The first attempt runs behind the caller. The retry waits on the clock.
1010
+ await simAws.backgroundTasksComplete();
1011
+ console.log(attempts.length);
1012
+
1013
+ await simAws.clock().advanceBy({ minutes: 2 });
1014
+ console.log(attempts.length);
1015
+
1016
+ const received = await sqs.receiveMessage(
1017
+ new ReceiveMessageCommand({ QueueUrl: created.QueueUrl }),
1018
+ );
1019
+ const record = JSON.parse(
1020
+ String(received.Messages?.[0]?.Body),
1021
+ ) as SimLambdaDestinationRecord;
1022
+ console.log(record.requestContext.condition);
1023
+ console.log(record.requestContext.approximateInvokeCount);
1024
+ console.log(record.requestPayload);
1025
+ ```
1026
+
1027
+ The queue receives the record real Lambda sends, carrying the event that was invoked as
1028
+ `requestPayload`, the handler error as `responsePayload`, how many attempts were made, and a
1029
+ `condition` of `Success`, `RetriesExhausted` or `EventAgeExceeded`. An event that outlives
1030
+ `MaximumEventAgeInSeconds` is given up on before its next attempt and reported under that last
1031
+ condition.
1032
+
1033
+ `GetFunctionEventInvokeConfigCommand`, `UpdateFunctionEventInvokeConfigCommand`,
1034
+ `DeleteFunctionEventInvokeConfigCommand` and `ListFunctionEventInvokeConfigsCommand` read and change
1035
+ what was written. `Put` writes the whole config, returning a setting it leaves out to its default,
1036
+ and `Update` changes only the settings it names. A `Qualifier` gives a published version or an alias
1037
+ a config of its own, and an invocation of a qualifier with no config of its own uses the function's.
1038
+
1039
+ ### Dead-letter targets
1040
+
1041
+ `DeadLetterConfig` is the older way to keep a failed asynchronous invocation, and simulated Lambda
1042
+ takes it on `CreateFunction` and `UpdateFunctionConfiguration`. Its `TargetArn` names a simulated
1043
+ SQS queue or SNS topic, which receives the event as it was invoked rather than the destination
1044
+ record around it.
1045
+
1046
+ ```typescript sim-lambda-dead-letter-queue
1047
+ /**
1048
+ * A simulated Lambda function with a dead-letter queue.
1049
+ */
1050
+
1051
+ import { CreateFunctionCommand, InvokeCommand } from "@aws-sdk/client-lambda";
1052
+ import { CreateQueueCommand, ReceiveMessageCommand } from "@aws-sdk/client-sqs";
1053
+
1054
+ import { SimAws } from "@kensio/yulin";
1055
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
1056
+
1057
+ const simAws = new SimAws();
1058
+ const lambda = simAws.lambda();
1059
+ const sqs = simAws.sqs();
1060
+
1061
+ const created = await sqs.createQueue(
1062
+ new CreateQueueCommand({ QueueName: "orders-dlq" }),
1063
+ );
1064
+
1065
+ await lambda.createFunction(
1066
+ new CreateFunctionCommand({
1067
+ FunctionName: "orders",
1068
+ Role: "arn:aws:iam::111111111111:role/OrdersRole",
1069
+ DeadLetterConfig: {
1070
+ TargetArn: "arn:aws:sqs:us-east-1:888888888888:orders-dlq",
1071
+ },
1072
+ Code: {
1073
+ ZipFile: makeLambdaZipFileInput(() => {
1074
+ throw new Error("orders handler failed");
1075
+ }),
1076
+ },
1077
+ }),
1078
+ );
1079
+
1080
+ await lambda.invoke(
1081
+ new InvokeCommand({
1082
+ FunctionName: "orders",
1083
+ InvocationType: "Event",
1084
+ Payload: JSON.stringify({ id: 7 }),
1085
+ }),
1086
+ );
1087
+
1088
+ // Past both retries, so the invocation has been given up on.
1089
+ await simAws.backgroundTasksComplete();
1090
+ await simAws.clock().advanceBy({ minutes: 5 });
1091
+
1092
+ const received = await sqs.receiveMessage(
1093
+ new ReceiveMessageCommand({ QueueUrl: created.QueueUrl }),
1094
+ );
1095
+ console.log(received.Messages?.[0]?.Body);
1096
+ ```
1097
+
1098
+ A function carrying both a dead-letter target and an `OnFailure` destination sends to both.
1099
+
1100
+ ## Versions and aliases
1101
+
1102
+ `PublishVersionCommand` copies a function as it stands and numbers the copy. The first is version
1103
+ `1`, and each later one counts up. The copy keeps the code, handler, timeout, memory and
1104
+ environment it was published with. `CreateAliasCommand` gives one of those versions a name, and
1105
+ `UpdateAliasCommand` moves the name to another version, so a deployment can repoint what callers
1106
+ invoke.
1107
+
1108
+ `InvokeCommand` and `GetFunctionCommand` take a `Qualifier` naming a version number or an alias.
1109
+ The same qualifier can travel on the `FunctionName` instead, appended to the name (`orders:live`)
1110
+ or to a function ARN. `ExecutedVersion` on the invocation output is the version number that ran,
1111
+ and an invocation through an alias reports the version behind it. Inside the handler, `context`
1112
+ carries that number as `functionVersion` and the qualified ARN as `invokedFunctionArn`.
1113
+
1114
+ ```typescript sim-lambda-versions-and-aliases
1115
+ /**
1116
+ * Publishing a simulated Lambda function version, pointing an alias at it,
1117
+ * invoking through the alias, and granting a permission on the alias alone.
1118
+ */
1119
+
1120
+ import {
1121
+ AddPermissionCommand,
1122
+ CreateAliasCommand,
1123
+ CreateFunctionCommand,
1124
+ GetPolicyCommand,
1125
+ InvokeCommand,
1126
+ ListVersionsByFunctionCommand,
1127
+ PublishVersionCommand,
1128
+ } from "@aws-sdk/client-lambda";
1129
+
1130
+ import { SimAws } from "@kensio/yulin";
1131
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
1132
+
1133
+ const simAws = new SimAws();
1134
+ const lambda = simAws.lambda();
1135
+
1136
+ await lambda.createFunction(
1137
+ new CreateFunctionCommand({
1138
+ FunctionName: "orders",
1139
+ Role: "arn:aws:iam::111111111111:role/OrdersRole",
1140
+ Code: {
1141
+ ZipFile: makeLambdaZipFileInput((_event, context) => ({
1142
+ ranAs: context.functionVersion,
1143
+ })),
1144
+ },
1145
+ }),
1146
+ );
1147
+
1148
+ const published = await lambda.publishVersion(
1149
+ new PublishVersionCommand({ FunctionName: "orders" }),
1150
+ );
1151
+ console.log(published.Version);
1152
+ console.log(published.FunctionArn);
1153
+
1154
+ await lambda.createAlias(
1155
+ new CreateAliasCommand({
1156
+ FunctionName: "orders",
1157
+ Name: "live",
1158
+ FunctionVersion: published.Version,
1159
+ }),
1160
+ );
1161
+
1162
+ const invoked = await lambda.invoke(
1163
+ new InvokeCommand({ FunctionName: "orders", Qualifier: "live" }),
1164
+ );
1165
+ console.log(invoked.ExecutedVersion);
1166
+
1167
+ const versions = await lambda.listVersionsByFunction(
1168
+ new ListVersionsByFunctionCommand({ FunctionName: "orders" }),
1169
+ );
1170
+ console.log(versions.Versions.map((version) => version.Version));
1171
+
1172
+ await lambda.addPermission(
1173
+ new AddPermissionCommand({
1174
+ FunctionName: "orders",
1175
+ Qualifier: "live",
1176
+ StatementId: "AllowReporting",
1177
+ Action: "lambda:InvokeFunction",
1178
+ Principal: "222222222222",
1179
+ }),
1180
+ );
1181
+
1182
+ // The statements granted on the alias, and on nothing else.
1183
+ const aliasPolicy = await lambda.getPolicy(
1184
+ new GetPolicyCommand({ FunctionName: "orders", Qualifier: "live" }),
1185
+ );
1186
+ console.log(aliasPolicy.Policy);
1187
+ ```
1188
+
1189
+ A function with no qualifier is `$LATEST`, which is what every caller that passes none reaches, and
1190
+ what `ListVersionsByFunctionCommand` lists ahead of the published versions. An alias points at a
1191
+ published version, and `$LATEST` is refused with a `ValidationException` on the version pattern, as
1192
+ it is on real Lambda. A qualifier naming no version and no alias fails with
1193
+ `ResourceNotFoundException` against the qualified ARN.
1194
+
1195
+ `GetAliasCommand`, `ListAliasesCommand` and `DeleteAliasCommand` read and remove aliases.
1196
+ `ListAliasesCommand` takes a `FunctionVersion` to narrow the answer to the aliases on one version,
1197
+ and a version nothing published is reported as missing rather than answered with an empty listing.
1198
+ Deleting an alias leaves the version it pointed at invokable by its number. Deleting the function
1199
+ takes its versions and aliases with it.
1200
+
1201
+ `AddPermissionCommand`, `RemovePermissionCommand` and `GetPolicyCommand` take the same `Qualifier`.
1202
+ The statement is held against the version or the alias it names, and carries that qualified ARN as
1203
+ its `Resource`. `GetPolicy` reads back the statements of the one resource it was asked for, and a
1204
+ request with no qualifier reads the function's own. An `Invoke` is authorized against the resource
1205
+ it names. A grant on `live` admits a call through `live`, and a call on the function itself or on
1206
+ the version behind the alias needs a grant of its own. See
1207
+ [Resource-based policies](#resource-based-policies).
1208
+
1209
+ An alias keeps the grants made on it when `UpdateAliasCommand` moves it to another version. That is
1210
+ what makes an alias a stable thing for another Account or another service to be granted (the grant
1211
+ belongs to the name that was integrated against).
1212
+
1213
+ The version and alias commands act on the function itself, so they take its name or its unqualified
1214
+ ARN. A `FunctionName` carrying a qualifier (`orders:live`) is refused with an
1215
+ `InvalidParameterValueException` rather than acted on as though the qualifier were absent.
1216
+
1217
+ ## Triggering a function from an SQS queue
1218
+
1219
+ An event source mapping connects a [simulated queue](https://yulinsim.dev/services/sqs/ "Simulated SQS docs") to a function.
1220
+ Messages sent to the queue are delivered to the handler as an SQS event, with the `Records` shape
1221
+ real Lambda uses.
1222
+
1223
+ Polling runs on the simulation's background scheduler. A test awaits
1224
+ `simAws.backgroundTasksComplete()` and then asserts, without sleeping.
1225
+
1226
+ `BatchSize` says how many messages one invocation may be given, and defaults to 10. The queue and
1227
+ the function have to be in the same account and region, as they do on real AWS.
1228
+
1229
+ ```typescript sim-lambda-sqs-event-source
1230
+ /**
1231
+ * Delivering messages from a simulated queue to a simulated function.
1232
+ */
1233
+
1234
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
1235
+ import {
1236
+ CreateEventSourceMappingCommand,
1237
+ CreateFunctionCommand,
1238
+ } from "@aws-sdk/client-lambda";
1239
+ import { CreateQueueCommand, SendMessageCommand } from "@aws-sdk/client-sqs";
1240
+
1241
+ import { SimAws } from "@kensio/yulin";
1242
+ import {
1243
+ makeLambdaZipFileInput,
1244
+ type SimLambdaSqsEvent,
1245
+ } from "@kensio/yulin/lambda";
1246
+
1247
+ const simAws = new SimAws();
1248
+ const queueArn = `arn:aws:sqs:${simAws.defaultRegionName}:${simAws.defaultAccountId}:orders`;
1249
+
1250
+ const { QueueUrl } = await simAws
1251
+ .sqs()
1252
+ .createQueue(new CreateQueueCommand({ QueueName: "orders" }));
1253
+
1254
+ // The execution role needs the three SQS actions Lambda polls a queue with.
1255
+ const role = await simAws.iam().createRole(
1256
+ new CreateRoleCommand({
1257
+ RoleName: "OrderConsumerRole",
1258
+ AssumeRolePolicyDocument: JSON.stringify({
1259
+ Version: "2012-10-17",
1260
+ Statement: {
1261
+ Effect: "Allow",
1262
+ Principal: { Service: "lambda.amazonaws.com" },
1263
+ Action: "sts:AssumeRole",
1264
+ },
1265
+ }),
1266
+ }),
1267
+ );
1268
+
1269
+ await simAws.iam().putRolePolicy(
1270
+ new PutRolePolicyCommand({
1271
+ RoleName: "OrderConsumerRole",
1272
+ PolicyName: "ConsumeOrders",
1273
+ PolicyDocument: JSON.stringify({
1274
+ Version: "2012-10-17",
1275
+ Statement: {
1276
+ Effect: "Allow",
1277
+ Action: [
1278
+ "sqs:ReceiveMessage",
1279
+ "sqs:DeleteMessage",
1280
+ "sqs:GetQueueAttributes",
1281
+ ],
1282
+ Resource: queueArn,
1283
+ },
1284
+ }),
1285
+ }),
1286
+ );
1287
+
1288
+ const consumed: string[] = [];
1289
+
1290
+ await simAws.lambda().createFunction(
1291
+ new CreateFunctionCommand({
1292
+ FunctionName: "order-consumer",
1293
+ Role: role.Role.Arn,
1294
+ Code: {
1295
+ ZipFile: makeLambdaZipFileInput((event: SimLambdaSqsEvent) => {
1296
+ for (const record of event.Records) {
1297
+ consumed.push(record.body);
1298
+ }
1299
+ }),
1300
+ },
1301
+ }),
1302
+ );
1303
+
1304
+ await simAws.lambda().createEventSourceMapping(
1305
+ new CreateEventSourceMappingCommand({
1306
+ EventSourceArn: queueArn,
1307
+ FunctionName: "order-consumer",
1308
+ BatchSize: 5,
1309
+ }),
1310
+ );
1311
+
1312
+ await simAws
1313
+ .sqs()
1314
+ .sendMessage(new SendMessageCommand({ QueueUrl, MessageBody: "order-1" }));
1315
+
1316
+ // Delivery happens in the background, so wait for the simulation to settle.
1317
+ await simAws.backgroundTasksComplete();
1318
+
1319
+ console.log(consumed); // ["order-1"]
1320
+ ```
1321
+
1322
+ Each record carries `messageId`, `receiptHandle`, `body`, `md5OfBody`, `messageAttributes`,
1323
+ `eventSource`, `eventSourceARN`, `awsRegion`, and the `attributes` map holding `SentTimestamp`,
1324
+ `ApproximateReceiveCount` and `ApproximateFirstReceiveTimestamp`. `SimLambdaSqsEvent` and
1325
+ `SimLambdaSqsEventRecord` are exported from `@kensio/yulin/lambda` for typing a handler, and are
1326
+ minimal structural equivalents of the `SQSEvent` and `SQSRecord` types from the `aws-lambda` typings
1327
+ package, and a handler already written against those can be passed in unchanged. `SenderId` is left
1328
+ out, because a simulated caller has no user or role id to report it as.
1329
+
1330
+ Creating the mapping checks the two things real Lambda checks. The queue has to exist, and the
1331
+ function's execution role has to be allowed `sqs:ReceiveMessage`, `sqs:DeleteMessage` and
1332
+ `sqs:GetQueueAttributes` on it. A role missing one of them fails with
1333
+ `InvalidParameterValueException` naming the operation, before the mapping exists.
1334
+
1335
+ `GetEventSourceMappingCommand`, `ListEventSourceMappingsCommand` and
1336
+ `DeleteEventSourceMappingCommand` read and remove mappings. A mapping is `Creating` when the command
1337
+ returns and `Enabled` once the simulation has caught up, as on real Lambda. Deleting it stops the
1338
+ polling.
1339
+
1340
+ ### Mapping onto a version or an alias
1341
+
1342
+ A `FunctionName` can carry a version number or an alias name on the end, or be a qualified function
1343
+ ARN, and batches then go to the version that qualifier names. `FunctionArn` on the mapping is the
1344
+ alias, and the version behind it is what runs:
1345
+
1346
+ ```typescript sim-lambda-event-source-alias
1347
+ /**
1348
+ * An event source mapping onto an alias, polling for the version it points at.
1349
+ */
1350
+
1351
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
1352
+ import {
1353
+ CreateAliasCommand,
1354
+ CreateEventSourceMappingCommand,
1355
+ CreateFunctionCommand,
1356
+ PublishVersionCommand,
1357
+ } from "@aws-sdk/client-lambda";
1358
+ import { CreateQueueCommand, SendMessageCommand } from "@aws-sdk/client-sqs";
1359
+
1360
+ import { SimAws } from "@kensio/yulin";
1361
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
1362
+
1363
+ const simAws = new SimAws();
1364
+ const lambda = simAws.lambda();
1365
+ const queueArn = `arn:aws:sqs:${simAws.defaultRegionName}:${simAws.defaultAccountId}:orders`;
1366
+
1367
+ const { QueueUrl } = await simAws
1368
+ .sqs()
1369
+ .createQueue(new CreateQueueCommand({ QueueName: "orders" }));
1370
+
1371
+ const role = await simAws.iam().createRole(
1372
+ new CreateRoleCommand({
1373
+ RoleName: "OrderConsumerRole",
1374
+ AssumeRolePolicyDocument: JSON.stringify({
1375
+ Version: "2012-10-17",
1376
+ Statement: {
1377
+ Effect: "Allow",
1378
+ Principal: { Service: "lambda.amazonaws.com" },
1379
+ Action: "sts:AssumeRole",
1380
+ },
1381
+ }),
1382
+ }),
1383
+ );
1384
+
1385
+ await simAws.iam().putRolePolicy(
1386
+ new PutRolePolicyCommand({
1387
+ RoleName: "OrderConsumerRole",
1388
+ PolicyName: "ConsumeOrders",
1389
+ PolicyDocument: JSON.stringify({
1390
+ Version: "2012-10-17",
1391
+ Statement: {
1392
+ Effect: "Allow",
1393
+ Action: [
1394
+ "sqs:ReceiveMessage",
1395
+ "sqs:DeleteMessage",
1396
+ "sqs:GetQueueAttributes",
1397
+ ],
1398
+ Resource: queueArn,
1399
+ },
1400
+ }),
1401
+ }),
1402
+ );
1403
+
1404
+ await lambda.createFunction(
1405
+ new CreateFunctionCommand({
1406
+ FunctionName: "order-consumer",
1407
+ Role: role.Role.Arn,
1408
+ Code: {
1409
+ ZipFile: makeLambdaZipFileInput((_event, context) => {
1410
+ console.log(context.functionVersion); // "1", the version behind `live`
1411
+
1412
+ return "handled";
1413
+ }),
1414
+ },
1415
+ }),
1416
+ );
1417
+
1418
+ const published = await lambda.publishVersion(
1419
+ new PublishVersionCommand({ FunctionName: "order-consumer" }),
1420
+ );
1421
+
1422
+ await lambda.createAlias(
1423
+ new CreateAliasCommand({
1424
+ FunctionName: "order-consumer",
1425
+ Name: "live",
1426
+ FunctionVersion: published.Version,
1427
+ }),
1428
+ );
1429
+
1430
+ const mapping = await lambda.createEventSourceMapping(
1431
+ new CreateEventSourceMappingCommand({
1432
+ EventSourceArn: queueArn,
1433
+ FunctionName: "order-consumer:live",
1434
+ }),
1435
+ );
1436
+
1437
+ console.log(mapping.FunctionArn); // ...:function:order-consumer:live
1438
+
1439
+ await simAws
1440
+ .sqs()
1441
+ .sendMessage(new SendMessageCommand({ QueueUrl, MessageBody: "order-1" }));
1442
+ await simAws.backgroundTasksComplete();
1443
+ ```
1444
+
1445
+ The qualifier is resolved on every poll, so `UpdateAlias` moves what an existing mapping delivers to.
1446
+ The version has to be there when the mapping is made. A qualifier naming none fails with
1447
+ `ResourceNotFoundException`, the way a function that is not there does. Polling still runs as the
1448
+ function's execution role, which a published version carries over from the function it was published
1449
+ from.
1450
+
1451
+ ### When the handler fails
1452
+
1453
+ A handler that returns normally has handled the batch, and the messages are deleted from the queue.
1454
+ A handler that throws returns the whole batch. The messages stay on the queue, hidden until their
1455
+ visibility timeout lapses, and are delivered again after that. Advancing the simulation's clock is
1456
+ what brings them back:
1457
+
1458
+ ```typescript
1459
+ await simAws.clock().advanceBy({ seconds: 31 });
1460
+ ```
1461
+
1462
+ A queue with a `RedrivePolicy` eventually gives up on a message the handler keeps throwing on and
1463
+ moves it to the dead-letter queue, exactly as it would for any other failing consumer. See
1464
+ [dead-letter queues](https://yulinsim.dev/services/sqs/#dead-letter-queues "Simulated SQS dead-letter queue docs").
1465
+
1466
+ The handler error itself goes unreported to whoever sent the message, as it does on real AWS. What
1467
+ the sender sees is the message coming back.
1468
+
1469
+ ### Reporting individual message failures
1470
+
1471
+ A queue mapping created with `FunctionResponseTypes: ["ReportBatchItemFailures"]` takes the
1472
+ `batchItemFailures` list the handler returns. The message ids named in it go back to the queue, and
1473
+ the rest of the batch is deleted. A stream mapping takes the same list and does something else with
1474
+ it, under [reporting individual record failures](#reporting-individual-record-failures).
1475
+
1476
+ ```typescript
1477
+ await simAws.lambda().createEventSourceMapping(
1478
+ new CreateEventSourceMappingCommand({
1479
+ EventSourceArn: queueArn,
1480
+ FunctionName: "order-consumer",
1481
+ FunctionResponseTypes: ["ReportBatchItemFailures"],
1482
+ }),
1483
+ );
1484
+
1485
+ // The handler reports the message ids it could not handle.
1486
+ const handler = (event: SimLambdaSqsEvent) => ({
1487
+ batchItemFailures: event.Records.filter((record) => !canHandle(record)).map(
1488
+ (record) => ({ itemIdentifier: record.messageId }),
1489
+ ),
1490
+ });
1491
+ ```
1492
+
1493
+ A report naming an id that was not in the batch returns the whole batch, as real Lambda does with a
1494
+ report it cannot trust. So does an entry with no `itemIdentifier`. A handler that returns no list,
1495
+ or an empty `batchItemFailures` list, has handled the whole batch.
1496
+
1497
+ ### Making an SQS event without a queue
1498
+
1499
+ A test of the handler on its own, with no queue and no mapping, still has to pass it a whole event.
1500
+ `lambdaSqsEventFactory` makes one, and `lambdaSqsEventRecordFactory` makes the records in it. Such a
1501
+ test then says what the messages carry and leaves the rest to the factory:
1502
+
1503
+ ```typescript sim-lambda-sqs-event-factory
1504
+ /**
1505
+ * Making an SQS event to call a handler with.
1506
+ */
1507
+
1508
+ import {
1509
+ lambdaSqsEventFactory,
1510
+ lambdaSqsEventRecordFactory,
1511
+ type SimLambdaSqsEvent,
1512
+ } from "@kensio/yulin/lambda";
1513
+
1514
+ function ordersHandler(event: SimLambdaSqsEvent): readonly string[] {
1515
+ return event.Records.map(
1516
+ (record) => (JSON.parse(record.body) as { orderId: string }).orderId,
1517
+ );
1518
+ }
1519
+
1520
+ const batch = lambdaSqsEventFactory.make({
1521
+ Records: [{ body: '{"orderId":"YL-1"}' }, { body: '{"orderId":"YL-2"}' }],
1522
+ });
1523
+
1524
+ // [ 'YL-1', 'YL-2' ]
1525
+ console.log(ordersHandler(batch));
1526
+
1527
+ // The record factory makes one on its own, for a test about a single message.
1528
+ const record = lambdaSqsEventRecordFactory.make({
1529
+ body: '{"orderId":"YL-9"}',
1530
+ eventSourceARN: "arn:aws:sqs:eu-west-2:888888888888:orders",
1531
+ });
1532
+
1533
+ // eu-west-2
1534
+ console.log(record.awsRegion);
1535
+ ```
1536
+
1537
+ The default is the single-message batch a quiet queue delivers. Each record is completed as a
1538
+ delivered one is, including the message id, the receipt handle and the three system `attributes` a
1539
+ simulated mapping reports. Two fields a record repeats are computed from the rest. `md5OfBody` is
1540
+ the digest of the body given, which a handler checking the digest compares against, and `awsRegion`
1541
+ is the Region of the queue ARN given.
1542
+
1543
+ Reporting individual failures can be tested against a made event too. The handler's
1544
+ `batchItemFailures` name `messageId` values, and those are the ids of the records the factory made.
1545
+
1546
+ ### Event source mappings in templates
1547
+
1548
+ `AWS::Lambda::EventSourceMapping` deploys the same thing, and CDK's
1549
+ `fn.addEventSource(new SqsEventSource(queue))` emits one. `EventSourceArn` and `FunctionName` accept the
1550
+ `Fn::GetAtt` and `Ref` values a template gives them, `Ref` on the mapping returns its UUID, and
1551
+ `Fn::GetAtt` exposes `Id` and `EventSourceMappingArn`. A queue mapping has no `StartingPosition`, and
1552
+ a template naming one is refused. The same Resource deploys a
1553
+ [stream mapping](#stream-mappings-in-templates), which has to have one.
1554
+
1555
+ ## Triggering a function from a DynamoDB stream
1556
+
1557
+ An event source mapping also connects a [simulated table's stream](https://yulinsim.dev/services/dynamodb/#capturing-changes-with-a-stream "Simulated DynamoDB streams docs")
1558
+ to a function. Changes to the table are delivered to the handler as a DynamoDB stream event, with
1559
+ the `Records` shape real Lambda uses.
1560
+
1561
+ `StartingPosition` is required for a stream and is `TRIM_HORIZON` or `LATEST`. `TRIM_HORIZON` reads
1562
+ what the stream still holds, so changes made before the mapping existed are delivered too. `LATEST`
1563
+ reads only what the table changes from the moment the mapping starts reading. `AT_TIMESTAMP` is for
1564
+ [a Kinesis stream](#triggering-a-function-from-a-kinesis-stream) and is refused by name here.
1565
+
1566
+ `BatchSize` says how many records one invocation may be given, and defaults to 100, the number
1567
+ CDK's `DynamoEventSource` also asks for. The table and the function have to be in the same account
1568
+ and region, as they do on real AWS.
1569
+
1570
+ ```typescript sim-lambda-dynamodb-stream-event-source
1571
+ /**
1572
+ * Delivering a simulated table's changes to a simulated function.
1573
+ */
1574
+
1575
+ import {
1576
+ CreateTableCommand,
1577
+ GetItemCommand,
1578
+ PutItemCommand,
1579
+ } from "@aws-sdk/client-dynamodb";
1580
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
1581
+ import {
1582
+ CreateEventSourceMappingCommand,
1583
+ CreateFunctionCommand,
1584
+ } from "@aws-sdk/client-lambda";
1585
+
1586
+ import { SimAws } from "@kensio/yulin";
1587
+ import {
1588
+ makeLambdaZipFileInput,
1589
+ type SimLambdaDynamoDbStreamEvent,
1590
+ } from "@kensio/yulin/lambda";
1591
+
1592
+ const simAws = new SimAws();
1593
+
1594
+ const { TableDescription } = await simAws.dynamoDb().createTable(
1595
+ new CreateTableCommand({
1596
+ TableName: "orders",
1597
+ KeySchema: [{ AttributeName: "orderId", KeyType: "HASH" }],
1598
+ AttributeDefinitions: [{ AttributeName: "orderId", AttributeType: "S" }],
1599
+ BillingMode: "PAY_PER_REQUEST",
1600
+ StreamSpecification: {
1601
+ StreamEnabled: true,
1602
+ StreamViewType: "NEW_AND_OLD_IMAGES",
1603
+ },
1604
+ }),
1605
+ );
1606
+
1607
+ const streamArn = TableDescription?.LatestStreamArn;
1608
+
1609
+ // The projection goes into a second table. A function writing back into the
1610
+ // table whose stream invoked it would be delivered its own writes, which the
1611
+ // simulator refuses rather than looping on.
1612
+ await simAws.dynamoDb().createTable(
1613
+ new CreateTableCommand({
1614
+ TableName: "order-totals",
1615
+ KeySchema: [{ AttributeName: "orderId", KeyType: "HASH" }],
1616
+ AttributeDefinitions: [{ AttributeName: "orderId", AttributeType: "S" }],
1617
+ BillingMode: "PAY_PER_REQUEST",
1618
+ }),
1619
+ );
1620
+
1621
+ // The execution role needs the three stream actions Lambda reads a stream
1622
+ // with, plus ListStreams, which is on every stream rather than on one, and
1623
+ // whatever the function itself does.
1624
+ const role = await simAws.iam().createRole(
1625
+ new CreateRoleCommand({
1626
+ RoleName: "OrderProjectorRole",
1627
+ AssumeRolePolicyDocument: JSON.stringify({
1628
+ Version: "2012-10-17",
1629
+ Statement: {
1630
+ Effect: "Allow",
1631
+ Principal: { Service: "lambda.amazonaws.com" },
1632
+ Action: "sts:AssumeRole",
1633
+ },
1634
+ }),
1635
+ }),
1636
+ );
1637
+
1638
+ await simAws.iam().putRolePolicy(
1639
+ new PutRolePolicyCommand({
1640
+ RoleName: "OrderProjectorRole",
1641
+ PolicyName: "ProjectOrders",
1642
+ PolicyDocument: JSON.stringify({
1643
+ Version: "2012-10-17",
1644
+ Statement: [
1645
+ {
1646
+ Effect: "Allow",
1647
+ Action: [
1648
+ "dynamodb:DescribeStream",
1649
+ "dynamodb:GetRecords",
1650
+ "dynamodb:GetShardIterator",
1651
+ ],
1652
+ Resource: streamArn,
1653
+ },
1654
+ { Effect: "Allow", Action: "dynamodb:ListStreams", Resource: "*" },
1655
+ {
1656
+ Effect: "Allow",
1657
+ Action: "dynamodb:PutItem",
1658
+ Resource: `arn:aws:dynamodb:${simAws.defaultRegionName}:${simAws.defaultAccountId}:table/order-totals`,
1659
+ },
1660
+ ],
1661
+ }),
1662
+ }),
1663
+ );
1664
+
1665
+ await simAws.lambda().createFunction(
1666
+ new CreateFunctionCommand({
1667
+ FunctionName: "order-projector",
1668
+ Role: role.Role.Arn,
1669
+ Code: {
1670
+ ZipFile: makeLambdaZipFileInput(
1671
+ async (event: SimLambdaDynamoDbStreamEvent) => {
1672
+ await Promise.all(
1673
+ event.Records.map(async (record) =>
1674
+ simAws.dynamoDb().putItem(
1675
+ new PutItemCommand({
1676
+ TableName: "order-totals",
1677
+ Item: {
1678
+ orderId: { S: record.dynamodb.Keys?.["orderId"]?.S ?? "" },
1679
+ total: { N: record.dynamodb.NewImage?.["total"]?.N ?? "0" },
1680
+ },
1681
+ }),
1682
+ ),
1683
+ ),
1684
+ );
1685
+ },
1686
+ ),
1687
+ },
1688
+ }),
1689
+ );
1690
+
1691
+ await simAws.lambda().createEventSourceMapping(
1692
+ new CreateEventSourceMappingCommand({
1693
+ EventSourceArn: streamArn,
1694
+ FunctionName: "order-projector",
1695
+ StartingPosition: "TRIM_HORIZON",
1696
+ }),
1697
+ );
1698
+
1699
+ await simAws.dynamoDb().putItem(
1700
+ new PutItemCommand({
1701
+ TableName: "orders",
1702
+ Item: { orderId: { S: "order-1" }, total: { N: "42" } },
1703
+ }),
1704
+ );
1705
+
1706
+ // Delivery happens in the background, so wait for the simulation to settle.
1707
+ await simAws.backgroundTasksComplete();
1708
+
1709
+ const projected = await simAws.dynamoDb().getItem(
1710
+ new GetItemCommand({
1711
+ TableName: "order-totals",
1712
+ Key: { orderId: { S: "order-1" } },
1713
+ }),
1714
+ );
1715
+
1716
+ console.log(projected.Item?.["total"]?.N); // "42"
1717
+ ```
1718
+
1719
+ Each record carries `eventID`, `eventName`, `eventVersion`, `eventSource`, `awsRegion`,
1720
+ `eventSourceARN` and a `dynamodb` body holding `Keys`, the images the stream's view type selects,
1721
+ `SequenceNumber`, `SizeBytes`, `StreamViewType` and `ApproximateCreationDateTime` as whole seconds
1722
+ since the epoch. A time to live removal also carries
1723
+ `userIdentity: { type: "Service", principalId: "dynamodb.amazonaws.com" }`. A handler tells an
1724
+ expiry from a deletion the application asked for by that. The event lower-cases those two fields
1725
+ where [the Streams API capitalizes them](https://yulinsim.dev/services/dynamodb/#reading-a-streams-records "Simulated DynamoDB Streams docs").
1726
+
1727
+ `SimLambdaDynamoDbStreamEvent` and `SimLambdaDynamoDbStreamEventRecord` are exported from
1728
+ `@kensio/yulin/lambda` for typing a handler, and are minimal structural equivalents of the
1729
+ `DynamoDBStreamEvent` and `DynamoDBRecord` types from the `aws-lambda` typings package.
1730
+
1731
+ A binary attribute reaches the handler as a base64 string, as it does on AWS. The event arrives as
1732
+ JSON, and JSON has no bytes. `Buffer.from(value.B, "base64")`
1733
+ therefore reads the same here as it does deployed. The
1734
+ [Streams API](https://yulinsim.dev/services/dynamodb/#reading-a-streams-records "Simulated DynamoDB Streams docs") hands out
1735
+ bytes for the same attribute, because its client decodes them. Nesting makes no difference, and
1736
+ binary inside a list or a map is encoded too.
1737
+
1738
+ Creating the mapping checks what real Lambda checks. The stream has to exist, and the function's
1739
+ execution role has to be allowed `dynamodb:DescribeStream`, `dynamodb:GetRecords` and
1740
+ `dynamodb:GetShardIterator` on it, plus `dynamodb:ListStreams` on `*`. Those four are what both the
1741
+ AWS managed policy and CDK's own grant give a stream consumer. A role missing one of them fails with
1742
+ `InvalidParameterValueException` naming the operation.
1743
+
1744
+ ### When the handler fails
1745
+
1746
+ A stream behaves differently from a queue here. A queue hands a message out and hides it, and a
1747
+ batch the handler threw on becomes the queue's problem afterwards. A stream hands out a place, and
1748
+ the mapping is the only thing that remembers it. A batch the handler threw on is read again from
1749
+ exactly where it was, and everything behind it waits until it is through. That is what blocking a
1750
+ shard means.
1751
+
1752
+ Advancing the simulation's clock is what hands the batch over again:
1753
+
1754
+ ```typescript
1755
+ await simAws.clock().advanceBy({ seconds: 30 });
1756
+ ```
1757
+
1758
+ The batch is delivered again five times, after 1, 2, 4, 8 and 16 seconds, so six deliveries in all.
1759
+ Then it is discarded and the mapping carries on with the stream, as AWS does once a stream mapping's
1760
+ error handling has run out.
1761
+
1762
+ Both the cadence and the number of attempts are simulator constraints rather than AWS behaviour. AWS
1763
+ documents no delay between attempts and retries until the records age out of the stream, a day
1764
+ later. A delay of zero here would fall due at the instant the clock already reads. A handler that
1765
+ always throws would then leave `advanceBy` with work falling due forever, and waiting out a
1766
+ simulated day is the same problem with more steps.
1767
+
1768
+ ### Reporting individual record failures
1769
+
1770
+ A stream mapping created with `FunctionResponseTypes: ["ReportBatchItemFailures"]` takes the
1771
+ `batchItemFailures` list the handler returns. For a stream the identifier is the record's
1772
+ `SequenceNumber`:
1773
+
1774
+ ```typescript
1775
+ await simAws.lambda().createEventSourceMapping(
1776
+ new CreateEventSourceMappingCommand({
1777
+ EventSourceArn: streamArn,
1778
+ FunctionName: "order-projector",
1779
+ StartingPosition: "TRIM_HORIZON",
1780
+ FunctionResponseTypes: ["ReportBatchItemFailures"],
1781
+ }),
1782
+ );
1783
+
1784
+ // The handler reports the sequence numbers it could not handle.
1785
+ const handler = (event: SimLambdaDynamoDbStreamEvent) => ({
1786
+ batchItemFailures: event.Records.filter((record) => !canHandle(record)).map(
1787
+ (record) => ({ itemIdentifier: record.dynamodb.SequenceNumber }),
1788
+ ),
1789
+ });
1790
+ ```
1791
+
1792
+ A stream reads that report differently from a queue. A queue takes back the messages the report
1793
+ names and deletes the rest. A stream moves its checkpoint to the lowest sequence
1794
+ number the report names and delivers everything from there again, including the records after it
1795
+ that the handler did handle. That is real AWS behaviour rather than a simulation artifact, and it is
1796
+ why a stream consumer has to be idempotent.
1797
+
1798
+ So a report naming only the last record of a batch delivers that record again. A report naming the
1799
+ first record delivers the whole batch again. The redelivery is a retry like any other. It waits out
1800
+ the same backoff, counts against the same five attempts, and what is left is discarded when they run
1801
+ out.
1802
+
1803
+ A report naming a sequence number that was not in the batch delivers the whole batch again, as real
1804
+ Lambda does with a report it cannot trust. So does an entry with no `itemIdentifier`. A handler that
1805
+ returns no list, or an empty `batchItemFailures` list, has handled the whole batch. A mapping
1806
+ created without `FunctionResponseTypes` ignores a report entirely.
1807
+
1808
+ ### Writing back to the source table
1809
+
1810
+ A handler that writes into the table whose stream invoked it is delivered its own write, which
1811
+ writes again. Real Lambda runs that loop for as long as the account is willing to pay for it. The
1812
+ simulation refuses instead, with an error naming the function, the stream and the table, before the
1813
+ test times out.
1814
+
1815
+ The write itself succeeds. The refusal comes afterwards, from whatever is waiting for the simulation
1816
+ to settle:
1817
+
1818
+ ```typescript
1819
+ await simAws.backgroundTasksComplete(); // throws SimLambdaStreamCascadeError
1820
+ ```
1821
+
1822
+ Only the handler's own writes count. Items written at the same time by the test, or by anything else
1823
+ in the simulation, are an ordinary batch however many of them there are, because the guard tells them
1824
+ apart by where the write came from, and never by when it landed.
1825
+
1826
+ Writing the projection into a second table is what the guard is asking for, and is what a real
1827
+ aggregation or search index does anyway.
1828
+
1829
+ ### Making a stream event without a table
1830
+
1831
+ `lambdaDynamoDbStreamEventFactory` and `lambdaDynamoDbStreamEventRecordFactory` make the same events
1832
+ for a test that calls the handler directly, with no table and no mapping:
1833
+
1834
+ ```typescript sim-lambda-dynamodb-stream-event-factory
1835
+ /**
1836
+ * Making a DynamoDB stream event to call a handler with.
1837
+ */
1838
+
1839
+ import {
1840
+ lambdaDynamoDbStreamEventFactory,
1841
+ type SimLambdaDynamoDbStreamEvent,
1842
+ } from "@kensio/yulin/lambda";
1843
+
1844
+ function shippedOrders(event: SimLambdaDynamoDbStreamEvent): readonly string[] {
1845
+ return event.Records.filter(
1846
+ (record) => record.dynamodb.NewImage?.["status"]?.S === "shipped",
1847
+ ).map((record) => record.dynamodb.Keys?.["orderId"]?.S ?? "");
1848
+ }
1849
+
1850
+ const event = lambdaDynamoDbStreamEventFactory.make({
1851
+ Records: [
1852
+ {
1853
+ eventName: "MODIFY",
1854
+ dynamodb: {
1855
+ Keys: { orderId: { S: "YL-1" } },
1856
+ OldImage: { orderId: { S: "YL-1" }, status: { S: "placed" } },
1857
+ NewImage: { orderId: { S: "YL-1" }, status: { S: "shipped" } },
1858
+ },
1859
+ },
1860
+ { eventName: "INSERT" },
1861
+ ],
1862
+ });
1863
+
1864
+ // [ 'YL-1' ]
1865
+ console.log(shippedOrders(event));
1866
+
1867
+ // NEW_AND_OLD_IMAGES, because that is what this record carries
1868
+ console.log(event.Records[0]?.dynamodb.StreamViewType);
1869
+ ```
1870
+
1871
+ The default is the single-record batch one changed item produces. What a record says in more than
1872
+ one place is computed from the rest, and the result is a record a stream could have delivered. An
1873
+ `INSERT` carries a new image, a `REMOVE` an old one and a `MODIFY` both, `StreamViewType` names the
1874
+ images
1875
+ the record actually carries, and `awsRegion` is the Region of the stream ARN given. A record for a
1876
+ `KEYS_ONLY` stream is one with the images explicitly taken away, as in
1877
+ `make({ dynamodb: { NewImage: undefined, OldImage: undefined } })`.
1878
+
1879
+ `SizeBytes` is a plausible default, and measures nothing about the images given. A test asserting
1880
+ on it should say what it expects.
1881
+
1882
+ ### Stream mappings in templates
1883
+
1884
+ `AWS::Lambda::EventSourceMapping` deploys a stream mapping too. `EventSourceArn` takes the
1885
+ `Fn::GetAtt … StreamArn` of a
1886
+ [streamed table](https://yulinsim.dev/services/dynamodb/#deploying-a-table-with-a-stream "Simulated DynamoDB table stream in CloudFormation docs")
1887
+ in the same template. That reference is also what makes the mapping wait for the table.
1888
+ `StartingPosition` is required, as it is for an SDK caller, and the properties go to
1889
+ `CreateEventSourceMapping` unjudged. A template that gets one wrong is refused in the words the
1890
+ command refuses it in.
1891
+
1892
+ ```typescript sim-lambda-cloudformation-dynamodb-event-source
1893
+ /**
1894
+ * Deploying a table's stream, a function, and the mapping between them.
1895
+ */
1896
+
1897
+ import { PutItemCommand } from "@aws-sdk/client-dynamodb";
1898
+
1899
+ import { SimAws } from "@kensio/yulin";
1900
+ import type { SimLambdaDynamoDbStreamEvent } from "@kensio/yulin/lambda";
1901
+
1902
+ const simAws = new SimAws();
1903
+ const projected: string[] = [];
1904
+
1905
+ const stack = await simAws.cloudFormation().deployTemplate({
1906
+ stackName: "orders-stack",
1907
+ template: {
1908
+ Resources: {
1909
+ OrdersTable: {
1910
+ Type: "AWS::DynamoDB::Table",
1911
+ Properties: {
1912
+ TableName: "orders",
1913
+ KeySchema: [{ AttributeName: "orderId", KeyType: "HASH" }],
1914
+ AttributeDefinitions: [
1915
+ { AttributeName: "orderId", AttributeType: "S" },
1916
+ ],
1917
+ BillingMode: "PAY_PER_REQUEST",
1918
+ StreamSpecification: { StreamViewType: "NEW_AND_OLD_IMAGES" },
1919
+ },
1920
+ },
1921
+ ProjectorRole: {
1922
+ Type: "AWS::IAM::Role",
1923
+ Properties: {
1924
+ RoleName: "OrderProjectorRole",
1925
+ AssumeRolePolicyDocument: {
1926
+ Version: "2012-10-17",
1927
+ Statement: [
1928
+ {
1929
+ Effect: "Allow",
1930
+ Principal: { Service: "lambda.amazonaws.com" },
1931
+ Action: "sts:AssumeRole",
1932
+ },
1933
+ ],
1934
+ },
1935
+ Policies: [
1936
+ {
1937
+ PolicyName: "ReadOrdersStream",
1938
+ PolicyDocument: {
1939
+ Version: "2012-10-17",
1940
+ Statement: [
1941
+ {
1942
+ Effect: "Allow",
1943
+ Action: [
1944
+ "dynamodb:DescribeStream",
1945
+ "dynamodb:GetRecords",
1946
+ "dynamodb:GetShardIterator",
1947
+ ],
1948
+ Resource: { "Fn::GetAtt": ["OrdersTable", "StreamArn"] },
1949
+ },
1950
+ {
1951
+ Effect: "Allow",
1952
+ Action: "dynamodb:ListStreams",
1953
+ Resource: "*",
1954
+ },
1955
+ ],
1956
+ },
1957
+ },
1958
+ ],
1959
+ },
1960
+ },
1961
+ ProjectorFunction: {
1962
+ Type: "AWS::Lambda::Function",
1963
+ Properties: {
1964
+ FunctionName: "order-projector",
1965
+ Role: { "Fn::GetAtt": ["ProjectorRole", "Arn"] },
1966
+ },
1967
+ },
1968
+ OrderProjectorMapping: {
1969
+ Type: "AWS::Lambda::EventSourceMapping",
1970
+ Properties: {
1971
+ EventSourceArn: { "Fn::GetAtt": ["OrdersTable", "StreamArn"] },
1972
+ FunctionName: { Ref: "ProjectorFunction" },
1973
+ BatchSize: 100,
1974
+ StartingPosition: "TRIM_HORIZON",
1975
+ },
1976
+ },
1977
+ },
1978
+ },
1979
+ bindings: [
1980
+ {
1981
+ logicalId: "ProjectorFunction",
1982
+ handler: (event: SimLambdaDynamoDbStreamEvent): void => {
1983
+ for (const record of event.Records) {
1984
+ projected.push(record.dynamodb.Keys?.["orderId"]?.S ?? "");
1985
+ }
1986
+ },
1987
+ },
1988
+ ],
1989
+ });
1990
+ await stack.waitForDeployComplete();
1991
+
1992
+ await simAws.dynamoDb().putItem(
1993
+ new PutItemCommand({
1994
+ TableName: "orders",
1995
+ Item: { orderId: { S: "order-1" }, total: { N: "101" } },
1996
+ }),
1997
+ );
1998
+ await simAws.backgroundTasksComplete();
1999
+
2000
+ console.log(projected); // ["order-1"]
2001
+ ```
2002
+
2003
+ CDK's `fn.addEventSource(new DynamoEventSource(table, { startingPosition }))` synthesises exactly
2004
+ this, and deploys without hand-editing. The grant CDK writes alongside it is an inline policy with
2005
+ the three stream actions on the stream ARN and `dynamodb:ListStreams` on every stream, exactly what
2006
+ the mapping's execution-role check is looking for.
2007
+
2008
+ The properties a non-default `DynamoEventSource` adds are refused by name. Those are
2009
+ `FilterCriteria`, `ParallelizationFactor`, `BisectBatchOnFunctionError`, `TumblingWindowInSeconds`
2010
+ and `DestinationConfig`.
2011
+
2012
+ A hand-written template or a SAM application usually gives the function the AWS managed policy
2013
+ `AWSLambdaDynamoDBExecutionRole` instead. Simulated IAM has no model for managed policy ARNs, so
2014
+ that role reaches the mapping with no stream permissions and the mapping is refused when it is
2015
+ created.
2016
+ Write the grant as an inline policy, as the example above and CDK both do.
2017
+
2018
+ ## Triggering a function from a Kinesis stream
2019
+
2020
+ An event source mapping also connects a [simulated Kinesis stream](https://yulinsim.dev/services/kinesis/ "Simulated Kinesis Data Streams docs")
2021
+ to a function. Records put onto the stream are delivered to the handler as a Kinesis event, with the
2022
+ `Records` shape real Lambda uses.
2023
+
2024
+ A Kinesis stream has as many shards as it was created with, and every one of them is read. Real
2025
+ Lambda runs a processor per shard, and so does this: each shard keeps its own place on the stream,
2026
+ delivers its own batches and backs off on its own when a batch fails. One invocation is given
2027
+ records from one shard, so a handler that has to see records in order should put them under one
2028
+ partition key, which is what puts them on one shard.
2029
+
2030
+ `StartingPosition` is required for a stream. A Kinesis stream takes all three positions.
2031
+ `TRIM_HORIZON` reads what the stream still holds, `LATEST` reads only what arrives from the moment
2032
+ the mapping starts reading, and `AT_TIMESTAMP` reads from the instant `StartingPositionTimestamp`
2033
+ names.
2034
+
2035
+ `BatchSize` says how many records one invocation may be given, and defaults to 100, the number CDK's
2036
+ `KinesisEventSource` also asks for. The stream and the function have to be in the same account and
2037
+ region, as they do on real AWS.
2038
+
2039
+ ```typescript sim-lambda-kinesis-event-source
2040
+ /**
2041
+ * Delivering a simulated Kinesis stream's records to a simulated function.
2042
+ */
2043
+
2044
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
2045
+ import { CreateStreamCommand, PutRecordCommand } from "@aws-sdk/client-kinesis";
2046
+ import {
2047
+ CreateEventSourceMappingCommand,
2048
+ CreateFunctionCommand,
2049
+ } from "@aws-sdk/client-lambda";
2050
+
2051
+ import { SimAws } from "@kensio/yulin";
2052
+ import {
2053
+ makeLambdaZipFileInput,
2054
+ type SimLambdaKinesisStreamEvent,
2055
+ } from "@kensio/yulin/lambda";
2056
+
2057
+ const simAws = new SimAws();
2058
+
2059
+ await simAws
2060
+ .kinesis()
2061
+ .createStream(new CreateStreamCommand({ StreamName: "orders" }));
2062
+
2063
+ const streamArn = `arn:aws:kinesis:${simAws.defaultRegionName}:${simAws.defaultAccountId}:stream/orders`;
2064
+
2065
+ const { Role } = await simAws.iam().createRole(
2066
+ new CreateRoleCommand({
2067
+ RoleName: "OrderProjectorRole",
2068
+ AssumeRolePolicyDocument: JSON.stringify({
2069
+ Version: "2012-10-17",
2070
+ Statement: {
2071
+ Effect: "Allow",
2072
+ Principal: { Service: "lambda.amazonaws.com" },
2073
+ Action: "sts:AssumeRole",
2074
+ },
2075
+ }),
2076
+ }),
2077
+ );
2078
+
2079
+ await simAws.iam().putRolePolicy(
2080
+ new PutRolePolicyCommand({
2081
+ RoleName: "OrderProjectorRole",
2082
+ PolicyName: "ReadOrders",
2083
+ PolicyDocument: JSON.stringify({
2084
+ Version: "2012-10-17",
2085
+ Statement: [
2086
+ {
2087
+ Effect: "Allow",
2088
+ Action: [
2089
+ "kinesis:DescribeStream",
2090
+ "kinesis:GetRecords",
2091
+ "kinesis:GetShardIterator",
2092
+ ],
2093
+ Resource: streamArn,
2094
+ },
2095
+ { Effect: "Allow", Action: "kinesis:ListStreams", Resource: "*" },
2096
+ ],
2097
+ }),
2098
+ }),
2099
+ );
2100
+
2101
+ const projected: string[] = [];
2102
+
2103
+ await simAws.lambda().createFunction(
2104
+ new CreateFunctionCommand({
2105
+ FunctionName: "order-projector",
2106
+ Role: Role.Arn,
2107
+ Code: {
2108
+ ZipFile: makeLambdaZipFileInput(
2109
+ (event: SimLambdaKinesisStreamEvent): void => {
2110
+ for (const record of event.Records) {
2111
+ // The payload arrives base64 encoded, as it does on AWS.
2112
+ projected.push(
2113
+ Buffer.from(record.kinesis.data, "base64").toString("utf8"),
2114
+ );
2115
+ }
2116
+ },
2117
+ ),
2118
+ },
2119
+ }),
2120
+ );
2121
+
2122
+ await simAws.lambda().createEventSourceMapping(
2123
+ new CreateEventSourceMappingCommand({
2124
+ EventSourceArn: streamArn,
2125
+ FunctionName: "order-projector",
2126
+ StartingPosition: "TRIM_HORIZON",
2127
+ }),
2128
+ );
2129
+
2130
+ await simAws.kinesis().putRecord(
2131
+ new PutRecordCommand({
2132
+ StreamName: "orders",
2133
+ PartitionKey: "customer-1",
2134
+ Data: new TextEncoder().encode('{"id":"order-1"}'),
2135
+ }),
2136
+ );
2137
+
2138
+ // Delivery happens in the background, so wait for the simulation to settle.
2139
+ await simAws.backgroundTasksComplete();
2140
+
2141
+ console.log(projected[0]); // {"id":"order-1"}
2142
+ ```
2143
+
2144
+ Each record carries `eventID`, `eventName`, `eventVersion`, `eventSource`, `awsRegion`,
2145
+ `eventSourceARN`, `invokeIdentityArn` and a `kinesis` body holding `kinesisSchemaVersion`,
2146
+ `partitionKey`, `sequenceNumber`, `data` and `approximateArrivalTimestamp`. `eventID` is the shard
2147
+ identifier and the sequence number joined by a colon, so it is unique across the whole stream rather
2148
+ than within one shard. `invokeIdentityArn` is the execution role the records were read with.
2149
+
2150
+ The two translations worth knowing are the payload and the instant. `data` is base64 of the bytes
2151
+ that were put, because the event is JSON and JSON has no bytes, and `approximateArrivalTimestamp` is
2152
+ seconds since the epoch where
2153
+ [the Kinesis API](https://yulinsim.dev/services/kinesis/#putting-a-record-and-reading-it-back "Simulated Kinesis docs") hands
2154
+ out a `Date`. Both are what a deployed function receives.
2155
+
2156
+ `SimLambdaKinesisStreamEvent` and `SimLambdaKinesisStreamEventRecord` are exported from
2157
+ `@kensio/yulin/lambda` for typing a handler, and are minimal structural equivalents of the
2158
+ `KinesisStreamEvent` and `KinesisStreamRecord` types from the `aws-lambda` typings package.
2159
+
2160
+ Creating the mapping checks what real Lambda checks. The stream has to exist, and the function's
2161
+ execution role has to be allowed `kinesis:DescribeStream`, `kinesis:GetRecords` and
2162
+ `kinesis:GetShardIterator` on it, plus `kinesis:ListStreams` on `*`. Those four are what both the
2163
+ AWS managed policy and CDK's own grant give a stream consumer. A role missing one of them fails with
2164
+ `InvalidParameterValueException` naming the operation.
2165
+
2166
+ `FunctionResponseTypes: ["ReportBatchItemFailures"]` works as it does for a DynamoDB stream. A
2167
+ handler names a record by its `sequenceNumber`, and the mapping goes back to the lowest one the
2168
+ report names, so that record and everything after it on that shard is delivered again. A failing
2169
+ batch blocks its own shard and no other.
2170
+
2171
+ A mapping naming an enhanced fan-out consumer ARN is refused, since consumers are unsimulated.
2172
+ `AWS::Lambda::EventSourceMapping` deploys a Kinesis mapping the same way it deploys a DynamoDB one.
2173
+
2174
+ ## Function URLs
2175
+
2176
+ A Function URL is an HTTP endpoint for one function. Creating one with
2177
+ `CreateFunctionUrlConfigCommand` returns an AWS-shaped endpoint:
2178
+
2179
+ ```text
2180
+ https://<url-id>.lambda-url.<region>.on.aws/
2181
+ ```
2182
+
2183
+ Serve that URL with `serveSimAws` and application code, a frontend dev server, or curl can make real
2184
+ HTTP requests to the function, alongside the other simulated services on the same local server. Pass
2185
+ the Function URL through `srv.localUrl(...)`, which keeps the endpoint's hostname but sends the
2186
+ request to the local server, in the same way it adapts simulated S3 website and CloudFront URLs.
2187
+
2188
+ ```typescript sim-lambda-function-url
2189
+ /**
2190
+ * Serving a simulated Lambda Function URL on localhost.
2191
+ */
2192
+
2193
+ import {
2194
+ CreateFunctionCommand,
2195
+ CreateFunctionUrlConfigCommand,
2196
+ } from "@aws-sdk/client-lambda";
2197
+
2198
+ import { SimAws } from "@kensio/yulin";
2199
+ import {
2200
+ type SimLambdaFunctionUrlEvent,
2201
+ makeLambdaZipFileInput,
2202
+ } from "@kensio/yulin/lambda";
2203
+ import { serveSimAws } from "@kensio/yulin/serve";
2204
+
2205
+ const simAws = new SimAws();
2206
+ const lambda = simAws.lambda();
2207
+
2208
+ await lambda.createFunction(
2209
+ new CreateFunctionCommand({
2210
+ FunctionName: "greeter",
2211
+ Role: "arn:aws:iam::111111111111:role/GreeterRole",
2212
+ Code: {
2213
+ ZipFile: makeLambdaZipFileInput((event: SimLambdaFunctionUrlEvent) => ({
2214
+ statusCode: 200,
2215
+ headers: { "content-type": "text/plain" },
2216
+ body: `Hello ${event.queryStringParameters?.["name"] ?? "world"}`,
2217
+ })),
2218
+ },
2219
+ }),
2220
+ );
2221
+
2222
+ const urlConfig = await lambda.createFunctionUrlConfig(
2223
+ new CreateFunctionUrlConfigCommand({
2224
+ FunctionName: "greeter",
2225
+ AuthType: "NONE",
2226
+ }),
2227
+ );
2228
+
2229
+ // https://<url-id>.lambda-url.us-east-1.on.aws/
2230
+ console.log(urlConfig.FunctionUrl);
2231
+
2232
+ const srv = await serveSimAws({ simAws });
2233
+
2234
+ try {
2235
+ const response = await fetch(
2236
+ srv.localUrl(`${urlConfig.FunctionUrl}greet?name=Yulin`),
2237
+ );
2238
+
2239
+ console.log(response.status);
2240
+ console.log(await response.text());
2241
+ } finally {
2242
+ await srv.close();
2243
+ }
2244
+ ```
2245
+
2246
+ On localhost the endpoint hostname becomes
2247
+ `<url-id>.lambda-url.<region>.sim-aws.localhost:<port>`, dropping the `.on.aws` tail the way
2248
+ simulated S3 endpoints drop `.amazonaws.com`. Requests are routed by that hostname. A Function URL
2249
+ created in a non-default account or region reaches the right function without any extra
2250
+ configuration.
2251
+
2252
+ ### The invocation event and the response
2253
+
2254
+ The handler receives the API Gateway HTTP API payload format 2.0 event that real Function URLs
2255
+ send, the only format they use:
2256
+
2257
+ ```json
2258
+ {
2259
+ "version": "2.0",
2260
+ "routeKey": "$default",
2261
+ "rawPath": "/greet",
2262
+ "rawQueryString": "name=Yulin",
2263
+ "headers": { "host": "...", "user-agent": "..." },
2264
+ "queryStringParameters": { "name": "Yulin" },
2265
+ "cookies": ["session=abc"],
2266
+ "requestContext": {
2267
+ "http": { "method": "GET", "path": "/greet", "sourceIp": "127.0.0.1" }
2268
+ },
2269
+ "isBase64Encoded": false
2270
+ }
2271
+ ```
2272
+
2273
+ Cookies arrive in their own `cookies` field rather than in `headers`, and a request body arrives on
2274
+ `body` as text or, for binary content types, as base64 with `isBase64Encoded` set.
2275
+
2276
+ A handler can answer in either of the two shapes real Lambda accepts:
2277
+
2278
+ - a structured response, recognised by its `statusCode`, whose `headers`, `body`, `cookies` (sent
2279
+ as `set-cookie` headers) and `isBase64Encoded` control the HTTP response
2280
+ - any other value, which becomes a `200` JSON response, as in
2281
+ `return { greeting: "hello" }`
2282
+
2283
+ If the handler throws, the endpoint answers `502` with an AWS-like error document. The handler's
2284
+ error stays visible to the test, as the thrown error would be through `InvokeCommand`.
2285
+
2286
+ ### Making an invocation event without a request
2287
+
2288
+ A test of the handler on its own, with no endpoint serving it, still has to pass it a whole event.
2289
+ `lambdaFunctionUrlEventFactory` makes one. Such a test then says what the request was and leaves the
2290
+ rest to the factory:
2291
+
2292
+ ```typescript sim-lambda-function-url-event-factory
2293
+ /**
2294
+ * Making a Lambda Function URL invocation event to call a handler with.
2295
+ */
2296
+
2297
+ import { VariantFactory } from "@kensio/part-factory";
2298
+
2299
+ import {
2300
+ lambdaFunctionUrlEventFactory,
2301
+ type SimLambdaFunctionUrlEvent,
2302
+ type SimLambdaFunctionUrlResult,
2303
+ } from "@kensio/yulin/lambda";
2304
+
2305
+ function greeter(event: SimLambdaFunctionUrlEvent): SimLambdaFunctionUrlResult {
2306
+ return {
2307
+ statusCode: 200,
2308
+ headers: { "content-type": "text/plain" },
2309
+ body: `Hello ${event.queryStringParameters?.["name"] ?? "world"}`,
2310
+ };
2311
+ }
2312
+
2313
+ const event = lambdaFunctionUrlEventFactory.make({
2314
+ rawPath: "/greet",
2315
+ rawQueryString: "name=Yulin",
2316
+ });
2317
+
2318
+ // Hello Yulin
2319
+ console.log(greeter(event).body);
2320
+
2321
+ // A named variation of a request is a VariantFactory around it, as with any
2322
+ // other @kensio/part-factory factory.
2323
+ const formPostFactory = new VariantFactory(lambdaFunctionUrlEventFactory, {
2324
+ headers: { "content-type": "application/x-www-form-urlencoded" },
2325
+ requestContext: { http: { method: "POST" } },
2326
+ });
2327
+
2328
+ const formPost = formPostFactory.make({
2329
+ rawPath: "/subscribe",
2330
+ body: "email=someone%40yulin.test",
2331
+ });
2332
+
2333
+ // POST /subscribe
2334
+ console.log(
2335
+ `${formPost.requestContext.http.method} ${formPost.requestContext.http.path}`,
2336
+ );
2337
+ ```
2338
+
2339
+ The defaults describe an anonymous `GET /` to a `NONE` auth Function URL, down to the headers AWS
2340
+ stamps on a proxied request. A handler reading `host`, `x-forwarded-for` or `x-amzn-trace-id` finds
2341
+ what it would find on AWS. The fields a Function URL invocation never carries (`pathParameters` and
2342
+ `stageVariables`) are absent, as they are in a served event.
2343
+
2344
+ A real event says several things twice, and the factory computes its defaults from the overrides so
2345
+ that supplying either copy sets both:
2346
+
2347
+ | What the request says | Where the event says it |
2348
+ | --------------------- | ------------------------------------------------------------------------------------------------- |
2349
+ | the path | `rawPath` and `requestContext.http.path` |
2350
+ | the query | `rawQueryString` and the parsed `queryStringParameters` |
2351
+ | the endpoint | `requestContext.apiId`, `requestContext.domainPrefix`, `requestContext.domainName`, `host` header |
2352
+ | the caller | `requestContext.http.sourceIp` and `userAgent`, the `x-forwarded-for` and `user-agent` headers |
2353
+ | the time | `requestContext.timeEpoch` and the Common Log Format `requestContext.time` |
2354
+
2355
+ So `make({ rawPath: "/user/status" })` is a request for `/user/status` in both places, and the
2356
+ request context follows the override. Overriding both copies with different values is still allowed,
2357
+ for a test that wants an event no real invocation produces.
2358
+
2359
+ The same events go to a handler through
2360
+ [the API Gateway HTTP API](https://yulinsim.dev/services/apigatewayv2/ "Simulated API Gateway HTTP API usage docs") too, where
2361
+ the route key, the stage and the path parameters are the endpoint's rather than a Function URL's
2362
+ `$default`. An event for one of those is this factory with those fields overridden.
2363
+
2364
+ ### Managing a Function URL
2365
+
2366
+ `GetFunctionUrlConfigCommand` reads the configuration back, `UpdateFunctionUrlConfigCommand`
2367
+ changes the `AuthType` or `InvokeMode` while keeping the same endpoint, and
2368
+ `DeleteFunctionUrlConfigCommand` removes it, after which the hostname stops resolving and returns
2369
+ `404`. `ListFunctionUrlConfigsCommand` lists what a function has, which is one configuration or
2370
+ none, since a function has at most one Function URL.
2371
+
2372
+ ### IAM-authenticated Function URLs
2373
+
2374
+ A URL created with `AuthType: "AWS_IAM"` invokes the function only for a caller allowed
2375
+ `lambda:InvokeFunctionUrl` on the function ARN, and answers `403` otherwise. That is a different
2376
+ action from `lambda:InvokeFunction`, which the Invoke API uses. Real AWS separates the two so a
2377
+ policy can grant the HTTP endpoint without granting the SDK operation, and a policy naming only one
2378
+ of them grants only that one here as well.
2379
+
2380
+ The caller comes from the request itself, through either a SigV4 signature or an `x-sim-aws-caller`
2381
+ header naming a principal directly. A request that offers no caller at all is anonymous, owns no
2382
+ policies, and is refused. See [callers of HTTP requests](https://yulinsim.dev/services/iam/#callers-of-http-requests) in the IAM docs for how
2383
+ that resolution works and how to sign a served request.
2384
+
2385
+ A grant conditioned on `AWS:SourceArn` or `AWS:SourceAccount` is evaluated against what the request
2386
+ says it is being made for. That is how a permission granting `cloudfront.amazonaws.com` names one
2387
+ Distribution. Sim CloudFront states that itself when it reaches a Function URL Origin through an
2388
+ origin access control, and a Function URL behind a Distribution runs for that Distribution and
2389
+ refuses everything else. See
2390
+ [origin access controls](https://yulinsim.dev/services/cloudfront/#origin-access-controls) in the CloudFront docs.
2391
+
2392
+ A request declaring what its body hashes to in an `x-amz-content-sha256` header is held to it,
2393
+ whichever method it used. The header is checked against the bytes that arrived, and a request
2394
+ declaring `UNSIGNED-PAYLOAD` is refused with `403` and `The request signature we calculated does not
2395
+ match the signature you provided`, because Lambda supports no unsigned payload. A request that
2396
+ declares no hash is invoked as before. That is what makes a POST through a CloudFront origin access
2397
+ control need the viewer's own digest. See
2398
+ [posting to a Function URL Origin](https://yulinsim.dev/services/cloudfront/#posting-to-a-function-url-origin) in the
2399
+ CloudFront docs.
2400
+
2401
+ CloudFront is the exception to the two actions being separate. A request from
2402
+ `cloudfront.amazonaws.com` is authorized against `lambda:InvokeFunctionUrl` and
2403
+ `lambda:InvokeFunction`, and needs a grant for both, exactly what real Lambda asks an origin access
2404
+ control for. A caller signing its own request still needs only `lambda:InvokeFunctionUrl`.
2405
+
2406
+ An `AWS_IAM` invocation carries its caller into the event as `requestContext.authorizer.iam`, the
2407
+ part a handler reads. The `authorizer` block is absent for a `NONE` invocation, as it is on real
2408
+ AWS, and `requestContext.accountId` carries the caller's Account, where a `NONE` invocation reports
2409
+ `anonymous`. The block is
2410
+ shared with simulated API Gateway HTTP APIs, whose JWT authorizers would fill a `jwt` member
2411
+ instead, and `iam` is optional on the type.
2412
+
2413
+ ```typescript sim-lambda-function-url-iam-auth
2414
+ /**
2415
+ * Invoking a simulated Lambda Function URL that requires IAM authentication.
2416
+ */
2417
+
2418
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
2419
+ import {
2420
+ CreateFunctionCommand,
2421
+ CreateFunctionUrlConfigCommand,
2422
+ } from "@aws-sdk/client-lambda";
2423
+
2424
+ import { SimAws } from "@kensio/yulin";
2425
+ import {
2426
+ type SimLambdaFunctionUrlEvent,
2427
+ makeLambdaZipFileInput,
2428
+ } from "@kensio/yulin/lambda";
2429
+ import { serveSimAws } from "@kensio/yulin/serve";
2430
+
2431
+ const simAws = new SimAws();
2432
+ const roleArn = "arn:aws:iam::888888888888:role/Reporter";
2433
+
2434
+ const created = await simAws.lambda().createFunction(
2435
+ new CreateFunctionCommand({
2436
+ FunctionName: "reporter",
2437
+ Role: "arn:aws:iam::888888888888:role/ReporterExecutionRole",
2438
+ Code: {
2439
+ ZipFile: makeLambdaZipFileInput((event: SimLambdaFunctionUrlEvent) => ({
2440
+ statusCode: 200,
2441
+ body: `called by ${event.requestContext.authorizer?.iam?.userArn ?? "nobody"}`,
2442
+ })),
2443
+ },
2444
+ }),
2445
+ );
2446
+
2447
+ const urlConfig = await simAws.lambda().createFunctionUrlConfig(
2448
+ new CreateFunctionUrlConfigCommand({
2449
+ FunctionName: "reporter",
2450
+ AuthType: "AWS_IAM",
2451
+ }),
2452
+ );
2453
+
2454
+ // The Role that is allowed to call the endpoint.
2455
+ await simAws.iam().createRole(
2456
+ new CreateRoleCommand({
2457
+ RoleName: "Reporter",
2458
+ AssumeRolePolicyDocument: JSON.stringify({
2459
+ Version: "2012-10-17",
2460
+ Statement: {
2461
+ Effect: "Allow",
2462
+ Principal: { AWS: "arn:aws:iam::888888888888:root" },
2463
+ Action: "sts:AssumeRole",
2464
+ },
2465
+ }),
2466
+ }),
2467
+ );
2468
+
2469
+ await simAws.iam().putRolePolicy(
2470
+ new PutRolePolicyCommand({
2471
+ RoleName: "Reporter",
2472
+ PolicyName: "InvokeReporterUrl",
2473
+ PolicyDocument: JSON.stringify({
2474
+ Version: "2012-10-17",
2475
+ Statement: {
2476
+ Effect: "Allow",
2477
+ Action: "lambda:InvokeFunctionUrl",
2478
+ Resource: created.FunctionArn,
2479
+ },
2480
+ }),
2481
+ }),
2482
+ );
2483
+
2484
+ const srv = await serveSimAws({ simAws });
2485
+
2486
+ try {
2487
+ const url = srv.localUrl(urlConfig.FunctionUrl);
2488
+
2489
+ // Unauthenticated, so anonymous, so refused.
2490
+ const refused = await fetch(url);
2491
+ console.log(refused.status); // 403
2492
+
2493
+ // Named as the Role that is allowed to invoke.
2494
+ const allowed = await fetch(url, {
2495
+ headers: { "x-sim-aws-caller": roleArn },
2496
+ });
2497
+
2498
+ console.log(allowed.status); // 200
2499
+ console.log(await allowed.text()); // called by arn:aws:iam::888888888888:role/Reporter
2500
+ } finally {
2501
+ await srv.close();
2502
+ }
2503
+ ```
2504
+
2505
+ ## Resource-based policies
2506
+
2507
+ A function's resource-based policy is the other half of Lambda authorization. An identity policy
2508
+ says what a principal may do, and a resource policy says who may act on the function. Either one is
2509
+ enough to allow a call within the same Account. A principal from another Account needs both, the
2510
+ grant on the function and an identity policy in its own Account allowing the action. That is how AWS
2511
+ decides a cross-Account request. See
2512
+ [Cross-Account requests](https://yulinsim.dev/services/iam/#cross-account-requests).
2513
+
2514
+ `AddPermissionCommand` grants a statement, `RemovePermissionCommand` revokes it by `StatementId`,
2515
+ and `GetPolicyCommand` returns the assembled document. `AddPermission` is a shorthand. Lambda expands
2516
+ its parts into one statement, and reading that statement back shows what the grant means:
2517
+
2518
+ ```typescript sim-lambda-add-permission
2519
+ /**
2520
+ * Granting another Account permission to invoke a simulated Lambda function.
2521
+ */
2522
+
2523
+ import {
2524
+ AddPermissionCommand,
2525
+ CreateFunctionCommand,
2526
+ GetPolicyCommand,
2527
+ } from "@aws-sdk/client-lambda";
2528
+
2529
+ import { SimAws } from "@kensio/yulin";
2530
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
2531
+
2532
+ const simAws = new SimAws();
2533
+
2534
+ await simAws.lambda().createFunction(
2535
+ new CreateFunctionCommand({
2536
+ FunctionName: "greeter",
2537
+ Role: "arn:aws:iam::888888888888:role/GreeterRole",
2538
+ Code: { ZipFile: makeLambdaZipFileInput(() => "hello") },
2539
+ }),
2540
+ );
2541
+
2542
+ const added = await simAws.lambda().addPermission(
2543
+ new AddPermissionCommand({
2544
+ FunctionName: "greeter",
2545
+ StatementId: "AllowOtherAccount",
2546
+ Action: "lambda:InvokeFunctionUrl",
2547
+ Principal: "222222222222",
2548
+ FunctionUrlAuthType: "AWS_IAM",
2549
+ }),
2550
+ );
2551
+
2552
+ // The statement the shorthand expanded into.
2553
+ console.log(added.Statement);
2554
+
2555
+ const policy = await simAws
2556
+ .lambda()
2557
+ .getPolicy(new GetPolicyCommand({ FunctionName: "greeter" }));
2558
+
2559
+ console.log(policy.Policy);
2560
+ ```
2561
+
2562
+ `Principal` takes the same shorthand real Lambda does, and is expanded the same way. A 12-digit
2563
+ Account id becomes `{"AWS": "arn:aws:iam::<id>:root"}`, an ARN becomes `{"AWS": "<arn>"}`, anything
2564
+ else is read as a service principal, and `*` stays `*`.
2565
+
2566
+ `FunctionUrlAuthType` becomes a `lambda:FunctionUrlAuthType` condition, evaluated when a Function
2567
+ URL is invoked. That is what a Function URL grant conditions on in practice. A permission granted
2568
+ for `AWS_IAM` leaves a URL later switched to `NONE` closed.
2569
+
2570
+ `SourceArn` becomes an `ArnLike` condition on `AWS:SourceArn`, and `SourceAccount` a `StringEquals`
2571
+ condition on `AWS:SourceAccount`. Both are evaluated when another simulated service invokes the
2572
+ function on a resource's behalf, and every simulated service that does so supplies them. That covers
2573
+ a simulated API Gateway HTTP API invoking it through a Lambda proxy integration or as a `REQUEST`
2574
+ authorizer, a simulated S3 Bucket delivering an event notification, a simulated SNS topic delivering
2575
+ a message, a simulated Cognito user pool running a Lambda trigger, and an EventBridge or ELBv2
2576
+ target. The source ARN is what that service is invoking the function for, and the source Account is
2577
+ that service's resource's own. See
2578
+ [Granting the API permission to invoke the function](https://yulinsim.dev/services/apigatewayv2/#granting-the-api-permission-to-invoke-the-function)
2579
+ and [Lambda triggers](https://yulinsim.dev/services/cognito/#lambda-triggers). A served Function URL request carries a
2580
+ source ARN when it says what it is being made for, which is how a CloudFront origin access control
2581
+ reaches one. A direct `Invoke` and an SQS event source mapping supply no value for either, and a
2582
+ statement carrying one matches no request of theirs.
2583
+
2584
+ `PrincipalOrgID` and `InvokedViaFunctionUrl` are written into the statement so `GetPolicy` reports
2585
+ the grant that was made. No value is supplied for them at request time, and a statement carrying one
2586
+ of those never matches.
2587
+
2588
+ All three commands take a `Qualifier` naming a published version or an alias, and each qualified
2589
+ resource holds its own policy. See
2590
+ [Versions and aliases](#versions-and-aliases) for what a grant on one covers.
2591
+
2592
+ A function with no grant on it has no policy at all. `GetPolicy` reports that as a
2593
+ `ResourceNotFoundException`, and never as an empty document. Granting a `StatementId` that is
2594
+ already in use is a `ResourceConflictException`, and removing one that was never granted is a
2595
+ `ResourceNotFoundException`, as on AWS.
2596
+
2597
+ ### Permissions in templates
2598
+
2599
+ `AWS::Lambda::Permission` creates the same permission from a CloudFormation template, which matters
2600
+ because CDK emits one for every `grantInvoke` and `grantInvokeUrl` to a principal outside the
2601
+ stack's own Account. The Resource has no `StatementId` property. CloudFormation names the statement
2602
+ after the logical ID, and so does this.
2603
+
2604
+ ```json
2605
+ {
2606
+ "AllowOtherAccount": {
2607
+ "Type": "AWS::Lambda::Permission",
2608
+ "Properties": {
2609
+ "FunctionName": { "Ref": "GreeterFunction" },
2610
+ "Action": "lambda:InvokeFunctionUrl",
2611
+ "Principal": "222222222222",
2612
+ "FunctionUrlAuthType": "AWS_IAM"
2613
+ }
2614
+ }
2615
+ }
2616
+ ```
2617
+
2618
+ `FunctionName` accepts either a `Ref` to the function, giving its name, or an `Fn::GetAtt` on it,
2619
+ giving the ARN. A synthesized CDK app deploys either way with no special casing.
2620
+
2621
+ A `FunctionName` carrying a version number or an alias name grants on that qualified resource, and
2622
+ the statement's `Resource` is the qualified ARN. This is how CDK writes `grantInvoke` on a
2623
+ `lambda.Alias`, and a grant made that way admits a call through the alias while `$LATEST` keeps a
2624
+ policy of its own. See [versions and aliases](#versions-and-aliases).
2625
+
2626
+ ## Environment variables
2627
+
2628
+ A function can declare its own environment variables with `Environment.Variables`, as on real
2629
+ Lambda. While the function runs, its code reads those variables from `process.env`, alongside the
2630
+ AWS-provided runtime variables (`AWS_REGION`, `AWS_LAMBDA_FUNCTION_NAME`, and the rest). Those
2631
+ include placeholder execution role credentials in `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and
2632
+ `AWS_SESSION_TOKEN`, where an AWS SDK in the function code finds credentials, as it does on real
2633
+ Lambda. Their values authorize nothing. A call from function code is attributed to the execution
2634
+ Role because the invocation is running as it.
2635
+
2636
+ ```typescript sim-lambda-environment-variables
2637
+ /**
2638
+ * Giving a simulated Lambda function its own environment variables, read by
2639
+ * a real in-process handler function.
2640
+ */
2641
+
2642
+ import { CreateFunctionCommand, InvokeCommand } from "@aws-sdk/client-lambda";
2643
+
2644
+ import { SimAws } from "@kensio/yulin";
2645
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
2646
+
2647
+ const simAws = new SimAws();
2648
+ const lambda = simAws.lambda();
2649
+
2650
+ await lambda.createFunction(
2651
+ new CreateFunctionCommand({
2652
+ FunctionName: "greeter",
2653
+ Role: "arn:aws:iam::111111111111:role/GreeterRole",
2654
+ Environment: {
2655
+ Variables: { GREETING: "Hello", TABLE_NAME: "widgets" },
2656
+ },
2657
+ Code: {
2658
+ ZipFile: makeLambdaZipFileInput((event: { name: string }) => ({
2659
+ // Read inside the handler, so this sees the function's own
2660
+ // variables rather than the ones the test process happens to have.
2661
+ message: `${process.env["GREETING"] ?? "Hi"} ${event.name}`,
2662
+ tableName: process.env["TABLE_NAME"],
2663
+ region: process.env["AWS_REGION"],
2664
+ })),
2665
+ },
2666
+ }),
2667
+ );
2668
+
2669
+ const invokeOutput = await lambda.invoke(
2670
+ new InvokeCommand({
2671
+ FunctionName: "greeter",
2672
+ Payload: JSON.stringify({ name: "Yulin" }),
2673
+ }),
2674
+ );
2675
+
2676
+ if (invokeOutput.Payload === undefined) throw new Error("No invoke Payload");
2677
+ // {"message":"Hello Yulin","tableName":"widgets","region":"eu-west-2"}
2678
+ console.log(Buffer.from(invokeOutput.Payload).toString());
2679
+ ```
2680
+
2681
+ A function that declares variables gets only those and the AWS-provided ones. Variables that happen
2682
+ to be set in the process running your tests stay invisible to it, and a function cannot accidentally
2683
+ pass because your shell or CI environment had the right variable set. Two functions declaring the
2684
+ same variable name with different values each see their own, including when their invocations
2685
+ overlap.
2686
+
2687
+ The same applies to zip-packaged code in the vm runtime and to functions deployed from an
2688
+ `AWS::Lambda::Function` template with an `Environment` property, including ones backed by an
2689
+ [executable binding](#executable-bindings).
2690
+
2691
+ A function backed by a real in-process handler and declaring no variables at all is the exception.
2692
+ It keeps reading the test process's environment, with the AWS-provided variables laid over it, since
2693
+ that is where such a handler's configuration comes from when nothing declares it. Declaring the
2694
+ variables on the function keeps the test explicit about where they came from. Zip-packaged code in
2695
+ the vm runtime gets the function's own variables either way, and never the test process's.
2696
+
2697
+ This is also how a function reaches something outside the simulation, such as a Redis or a
2698
+ Postgres. See [non-AWS dependencies](https://yulinsim.dev/non-aws-dependencies/).
2699
+
2700
+ Variable names are validated as on real AWS. A name must match the Lambda name pattern
2701
+ `[a-zA-Z]([a-zA-Z0-9_])+`, meaning it starts with a letter, is at least two characters, and otherwise
2702
+ holds letters, digits and underscores. A name breaking that pattern is rejected with
2703
+ `ValidationException`. The names Lambda reserves for the runtime (`AWS_REGION`,
2704
+ `AWS_LAMBDA_FUNCTION_NAME`, `LAMBDA_TASK_ROOT` and so on) cannot be declared, and are rejected with
2705
+ `InvalidParameterValueException`. As on AWS, the pattern is checked first, and a reserved name that
2706
+ also breaks it, such as `_HANDLER`, is reported as the constraint violation.
2707
+
2708
+ ### Read environment variables inside the handler
2709
+
2710
+ A function backed by a real in-process handler behaves differently here. That handler is an ordinary
2711
+ function in your test process rather than code loaded into a sandbox. It only gets the function's
2712
+ own `process.env` while it is actually running.
2713
+
2714
+ That means a variable read at module scope is read too early:
2715
+
2716
+ ```typescript
2717
+ // Evaluated when your test file imports this module, before any invocation,
2718
+ // so it sees the test process's environment, not the function's.
2719
+ const TABLE_NAME = process.env.TABLE_NAME;
2720
+
2721
+ export const handler = async () => {
2722
+ // Read during the invocation, so this sees the function's own value.
2723
+ return { tableName: process.env.TABLE_NAME };
2724
+ };
2725
+ ```
2726
+
2727
+ Moving the read inside the handler fixes it. Zip code in the vm runtime is unaffected, because it is
2728
+ imported at cold start, during an invocation.
2729
+
2730
+ This rarely comes up, because test suites commonly export the same variables they configure their
2731
+ functions with, and then a module-scope read gets the right value anyway. Sim Lambda warns on the
2732
+ console in the two situations where the difference changes what your code sees:
2733
+
2734
+ - a declared variable whose name the host process also sets, with a different value
2735
+ - two simulated functions declaring the same variable name with different values
2736
+
2737
+ ## The time inside a handler
2738
+
2739
+ A function runs on its simulation's clock, and so does the function code. `Date.now()` and
2740
+ `new Date()` inside a handler report simulated time. Freezing the clock gives an invocation a
2741
+ constant `Date.now()`, and advancing it changes what the next invocation reads.
2742
+ `context.getRemainingTimeInMillis()` counts down against the same clock. A frozen clock leaves a
2743
+ handler with a constant budget, where it would otherwise drain in real time.
2744
+
2745
+ Zip code gets this from its own vm sandbox. A real in-process handler gets it from a substituted
2746
+ global `Date` that reports the invocation's clock while an invocation is running and the host clock
2747
+ otherwise. A time read at module scope is therefore read too early, exactly as it is for environment
2748
+ variables. See [simulated time](https://yulinsim.dev/time/) for the whole picture, including where real
2749
+ AWS puts the time on the event.
2750
+
2751
+ ## CloudFormation functions
2752
+
2753
+ Sim CloudFormation can create Lambda functions from `AWS::Lambda::Function`, typically alongside a
2754
+ same-stack `AWS::IAM::Role` referenced as the execution role. Inline `ZipFile` template source is
2755
+ packaged and run in the vm runtime, exactly as if it had been zipped and passed to
2756
+ `CreateFunctionCommand`.
2757
+
2758
+ ```typescript sim-lambda-cloudformation-function
2759
+ /**
2760
+ * Creating an invokable Lambda function through simulated CloudFormation.
2761
+ */
2762
+
2763
+ import { InvokeCommand } from "@aws-sdk/client-lambda";
2764
+
2765
+ import { SimAws } from "@kensio/yulin";
2766
+
2767
+ const simAws = new SimAws();
2768
+
2769
+ const stack = await simAws.cloudFormation().deployTemplate({
2770
+ stackName: "greeter-stack",
2771
+ template: {
2772
+ Resources: {
2773
+ GreeterRole: {
2774
+ Type: "AWS::IAM::Role",
2775
+ Properties: {
2776
+ RoleName: "GreeterRole",
2777
+ AssumeRolePolicyDocument: {
2778
+ Version: "2012-10-17",
2779
+ Statement: [
2780
+ {
2781
+ Effect: "Allow",
2782
+ Principal: { Service: "lambda.amazonaws.com" },
2783
+ Action: "sts:AssumeRole",
2784
+ },
2785
+ ],
2786
+ },
2787
+ },
2788
+ },
2789
+ GreeterFunction: {
2790
+ Type: "AWS::Lambda::Function",
2791
+ Properties: {
2792
+ FunctionName: "greeter",
2793
+ Role: {
2794
+ "Fn::GetAtt": ["GreeterRole", "Arn"],
2795
+ },
2796
+ Handler: "index.handler",
2797
+ Runtime: "nodejs20.x",
2798
+ Code: {
2799
+ ZipFile:
2800
+ "exports.handler = async (event) => 'Hello ' + event.name;",
2801
+ },
2802
+ },
2803
+ },
2804
+ },
2805
+ Outputs: {
2806
+ FunctionName: {
2807
+ Value: {
2808
+ Ref: "GreeterFunction",
2809
+ },
2810
+ },
2811
+ FunctionArn: {
2812
+ Value: {
2813
+ "Fn::GetAtt": ["GreeterFunction", "Arn"],
2814
+ },
2815
+ },
2816
+ },
2817
+ },
2818
+ });
2819
+ await stack.waitForDeployComplete();
2820
+
2821
+ console.log(stack.output("FunctionName"));
2822
+ console.log(stack.output("FunctionArn"));
2823
+
2824
+ const output = await simAws.lambda().invoke(
2825
+ new InvokeCommand({
2826
+ FunctionName: "greeter",
2827
+ Payload: JSON.stringify({ name: "Yulin" }),
2828
+ }),
2829
+ );
2830
+
2831
+ if (output.Payload === undefined) throw new Error("No invoke Payload");
2832
+ console.log(Buffer.from(output.Payload).toString());
2833
+
2834
+ await simAws.backgroundTasksComplete();
2835
+ ```
2836
+
2837
+ For `AWS::Lambda::Function`, `Ref` returns the function name and `Fn::GetAtt` supports `Arn`.
2838
+
2839
+ Supported function properties:
2840
+
2841
+ - `FunctionName` (a function with none is named after the stack and the logical ID)
2842
+ - `Role` (typically a `Ref`/`Fn::GetAtt` to a same-stack `AWS::IAM::Role`, both resolving to the
2843
+ role's ARN)
2844
+ - `Code` (inline `ZipFile` source string, or `S3Bucket`/`S3Key` fetched from same-scope sim S3)
2845
+ - `Handler`
2846
+ - `Runtime`
2847
+ - `Description`
2848
+ - `Timeout`
2849
+ - `MemorySize`
2850
+ - `Environment`
2851
+ - `DeadLetterConfig`, whose `TargetArn` names a queue or a topic. See
2852
+ [retries and destinations in templates](#retries-and-destinations-in-templates)
2853
+
2854
+ A function with no `FunctionName` is named from the stack name, the logical ID and a tail derived
2855
+ from both. A `RatesFunction` in `orders-stack` becomes `orders-stack-RatesFunction-` and twelve more
2856
+ characters, where real CloudFormation ends the name in twelve random ones. The name is trimmed to
2857
+ the 64 characters a function name allows, and [the CloudFormation docs](https://yulinsim.dev/services/cloudformation/#names-cloudformation-generates "Names CloudFormation generates")
2858
+ cover how the stack name and the logical ID share what is left.
2859
+
2860
+ Code in a missing bucket fails the deploy AWS-style with a `NoSuchBucket` diagnostic.
2861
+
2862
+ ### CDK asset code
2863
+
2864
+ `lambda.Code.fromAsset(...)` and the constructs built on it stage function code in the CDK cloud
2865
+ assembly, and synthesize a `Code.S3Bucket`/`S3Key` pointing at the CDK bootstrap staging bucket.
2866
+ Deploying a synthesized template file with `deployTemplateFile` publishes the cloud assembly's
2867
+ assets into that bucket in sim S3 before creating any resource, mirroring the way a real
2868
+ `cdk deploy` runs `cdk-assets` before CloudFormation. Asset-bundled functions then resolve their
2869
+ code through the ordinary sim S3 fetch and run their real handler modules.
2870
+
2871
+ Both shapes of staged asset are published. A handler directory is zipped on the way into sim S3,
2872
+ as `cdk-assets` zips it on the way to a real bucket. An asset that is already an archive, such as
2873
+ `Code.fromAsset("handler.zip")` or a bundler's archived output, is published as it stands.
2874
+
2875
+ Asset code runs under the same rules as any other sim Lambda code. Modules are evaluated as
2876
+ CommonJS in a vm, so everything the handler imports has to be in the asset, and only Node.js
2877
+ runtimes are simulated.
2878
+
2879
+ Two cases are skipped with a diagnostic, and the stack deploys around them:
2880
+
2881
+ - A function declaring a non-Node.js `Runtime`, such as the Python provider function CDK
2882
+ synthesizes for `BucketDeployment`. Sim CloudFormation simulates that custom resource directly,
2883
+ so its provider never needs to run. Bind a real in-process handler to simulate a function whose
2884
+ runtime Yulin cannot run.
2885
+ - A CDK-shaped template deployed without its cloud assembly, such as a template object passed
2886
+ inline to `deployTemplate`, where there is no asset to publish and no staging bucket.
2887
+
2888
+ ### Container image functions
2889
+
2890
+ A function with `PackageType: Image` names a container image instead of code, as CDK's
2891
+ `DockerImageFunction` and `lambda.DockerImageCode` synthesize it. Yulin never reads an image, and
2892
+ has nothing to run. The Resource is skipped with a diagnostic naming the image, and the rest of the
2893
+ stack deploys.
2894
+
2895
+ There are two ways to give that function a real in-process handler to run instead, and they suit
2896
+ different shapes of test:
2897
+
2898
+ - Bind one to the function for this deploy, the same mechanism as
2899
+ [executable bindings](#executable-bindings) and shown below.
2900
+ - Register one as the image in a
2901
+ [simulated ECR repository](https://yulinsim.dev/services/ecr/ "Simulated ECR usage docs"). That is a standing statement about
2902
+ what the image is, made once and good for every stack that runs it, and for a function created
2903
+ directly through `CreateFunction`.
2904
+
2905
+ Either way the handler replaces the image, and the function is created and invoked like any other. A
2906
+ binding is looked at first, because it is about one deploy where a repository is about the image
2907
+ everywhere.
2908
+
2909
+ ```typescript sim-lambda-container-image-function
2910
+ /**
2911
+ * Simulating a container image Lambda function with a bound handler.
2912
+ */
2913
+
2914
+ import { InvokeCommand } from "@aws-sdk/client-lambda";
2915
+
2916
+ import { SimAws } from "@kensio/yulin";
2917
+
2918
+ const imageFunctionTemplate = {
2919
+ Resources: {
2920
+ OrdersFunction: {
2921
+ Type: "AWS::Lambda::Function",
2922
+ Properties: {
2923
+ FunctionName: "orders",
2924
+ Role: "arn:aws:iam::111111111111:role/OrdersRole",
2925
+ PackageType: "Image",
2926
+ Code: {
2927
+ ImageUri:
2928
+ "111111111111.dkr.ecr.eu-west-2.amazonaws.com/orders:latest",
2929
+ },
2930
+ },
2931
+ },
2932
+ },
2933
+ };
2934
+
2935
+ // Without a binding, the function is skipped and the stack still deploys.
2936
+ const skippedSimAws = new SimAws();
2937
+
2938
+ const skippedStack = await skippedSimAws.cloudFormation().deployTemplate({
2939
+ stackName: "orders-stack",
2940
+ template: imageFunctionTemplate,
2941
+ });
2942
+
2943
+ console.log(skippedStack.getResource("OrdersFunction")?.skippedReason);
2944
+
2945
+ await skippedSimAws.backgroundTasksComplete();
2946
+
2947
+ // With a binding, the handler replaces the image and the function runs.
2948
+ const simAws = new SimAws();
2949
+
2950
+ await simAws.cloudFormation().deployTemplate({
2951
+ stackName: "orders-stack",
2952
+ template: imageFunctionTemplate,
2953
+ bindings: [
2954
+ {
2955
+ logicalId: "OrdersFunction",
2956
+ handler: (event: { orderId: string }): string =>
2957
+ `Processed ${event.orderId}`,
2958
+ },
2959
+ ],
2960
+ });
2961
+
2962
+ const output = await simAws.lambda().invoke(
2963
+ new InvokeCommand({
2964
+ FunctionName: "orders",
2965
+ Payload: JSON.stringify({ orderId: "order-1" }),
2966
+ }),
2967
+ );
2968
+
2969
+ if (output.Payload === undefined) throw new Error("No invoke Payload");
2970
+ console.log(Buffer.from(output.Payload).toString());
2971
+
2972
+ await simAws.backgroundTasksComplete();
2973
+ ```
2974
+
2975
+ A function declaring `Code.ImageUri` without `PackageType` is treated the same way. `ImageConfig`
2976
+ is ignored, because `Command`, `EntryPoint` and `WorkingDirectory` have no meaning for a handler
2977
+ running in this process.
2978
+
2979
+ A binding can also name the image repository instead of the function, which covers every function
2980
+ running that image without repeating the binding per stack. See
2981
+ [binding by container image repository](#binding-by-container-image-repository).
2982
+
2983
+ The skip reason says what was looked for. An image whose repository no simulated ECR holds is
2984
+ reported apart from one whose repository holds no image, since those send you to different places.
2985
+ The first is a repository name disagreeing with the template, and the second a handler that was
2986
+ never registered.
2987
+
2988
+ ## Function URLs in templates
2989
+
2990
+ `AWS::Lambda::Url` creates a Function URL for a deployed function, and CDK's
2991
+ `Function.addFunctionUrl(...)` emits one. `TargetFunctionArn` accepts either an `Fn::GetAtt` ARN or a
2992
+ `Ref` to the function, and `Fn::GetAtt` on the URL exposes `FunctionUrl` and `FunctionArn`.
2993
+
2994
+ ```typescript sim-lambda-cloudformation-function-url
2995
+ /**
2996
+ * Deploying a simulated Lambda Function URL from a CloudFormation template.
2997
+ */
2998
+
2999
+ import { SimAws } from "@kensio/yulin";
3000
+ import { serveSimAws } from "@kensio/yulin/serve";
3001
+
3002
+ const simAws = new SimAws();
3003
+
3004
+ const stack = await simAws.cloudFormation().deployTemplate({
3005
+ stackName: "greeter-stack",
3006
+ template: {
3007
+ Resources: {
3008
+ GreeterRole: {
3009
+ Type: "AWS::IAM::Role",
3010
+ Properties: {
3011
+ RoleName: "GreeterRole",
3012
+ AssumeRolePolicyDocument: {
3013
+ Version: "2012-10-17",
3014
+ Statement: [
3015
+ {
3016
+ Effect: "Allow",
3017
+ Principal: { Service: "lambda.amazonaws.com" },
3018
+ Action: "sts:AssumeRole",
3019
+ },
3020
+ ],
3021
+ },
3022
+ },
3023
+ },
3024
+ GreeterFunction: {
3025
+ Type: "AWS::Lambda::Function",
3026
+ Properties: {
3027
+ FunctionName: "greeter",
3028
+ Role: { "Fn::GetAtt": ["GreeterRole", "Arn"] },
3029
+ Handler: "index.handler",
3030
+ Runtime: "nodejs22.x",
3031
+ Code: {
3032
+ ZipFile:
3033
+ "exports.handler = async (event) => " +
3034
+ "({ statusCode: 200, body: 'Hello ' + event.rawPath });",
3035
+ },
3036
+ },
3037
+ },
3038
+ GreeterUrl: {
3039
+ Type: "AWS::Lambda::Url",
3040
+ Properties: {
3041
+ TargetFunctionArn: { "Fn::GetAtt": ["GreeterFunction", "Arn"] },
3042
+ AuthType: "NONE",
3043
+ },
3044
+ },
3045
+ },
3046
+ Outputs: {
3047
+ GreeterFunctionUrl: {
3048
+ Value: { "Fn::GetAtt": ["GreeterUrl", "FunctionUrl"] },
3049
+ },
3050
+ },
3051
+ },
3052
+ });
3053
+ await stack.waitForDeployComplete();
3054
+
3055
+ const functionUrl = stack.output("GreeterFunctionUrl");
3056
+ const srv = await serveSimAws({ simAws });
3057
+
3058
+ try {
3059
+ const response = await fetch(srv.localUrl(`${functionUrl}hello`));
3060
+
3061
+ console.log(await response.text());
3062
+ } finally {
3063
+ await srv.close();
3064
+ }
3065
+ ```
3066
+
3067
+ CDK templates work the same way. Synth the app, deploy the template file, and read
3068
+ `functionUrl.url` from the stack outputs. CDK pairs a public Function URL with an
3069
+ `AWS::Lambda::Permission`, which deploys alongside it as
3070
+ [permissions in templates](#permissions-in-templates) covers.
3071
+
3072
+ ## Versions and aliases in templates
3073
+
3074
+ `AWS::Lambda::Version` publishes a version of the function its `FunctionName` names, and
3075
+ `AWS::Lambda::Alias` gives one of those versions a name. CDK emits both for `fn.currentVersion` and
3076
+ `new lambda.Alias(...)`, and the integrations in such an app point at the alias.
3077
+
3078
+ `FunctionName` accepts either a `Ref` to the function, giving its name, or an `Fn::GetAtt` on it,
3079
+ giving the ARN. `Ref` on the version resolves to the qualified function ARN
3080
+ (`arn:aws:lambda:us-east-1:888888888888:function:greeter:1`) and `Fn::GetAtt` `Version` to the
3081
+ number on the end of it. An alias's `FunctionVersion` is usually written from that number. `Ref` on
3082
+ the alias resolves to the alias ARN. `Fn::GetAtt` also supports `FunctionArn` on a version and
3083
+ `AliasArn` on an alias.
3084
+
3085
+ ```typescript sim-lambda-cloudformation-alias
3086
+ /**
3087
+ * Deploying a Lambda version and an alias on it from a CloudFormation
3088
+ * template, and invoking the function through the alias.
3089
+ */
3090
+
3091
+ import { InvokeCommand } from "@aws-sdk/client-lambda";
3092
+
3093
+ import { SimAws } from "@kensio/yulin";
3094
+
3095
+ const simAws = new SimAws();
3096
+
3097
+ const stack = await simAws.cloudFormation().deployTemplate({
3098
+ stackName: "greeter-stack",
3099
+ template: {
3100
+ Resources: {
3101
+ GreeterFunction: {
3102
+ Type: "AWS::Lambda::Function",
3103
+ Properties: {
3104
+ FunctionName: "greeter",
3105
+ Role: "arn:aws:iam::111111111111:role/GreeterRole",
3106
+ Handler: "index.handler",
3107
+ Runtime: "nodejs22.x",
3108
+ Code: {
3109
+ ZipFile:
3110
+ "exports.handler = async (event, context) => " +
3111
+ "context.functionVersion;",
3112
+ },
3113
+ },
3114
+ },
3115
+ GreeterVersion: {
3116
+ Type: "AWS::Lambda::Version",
3117
+ Properties: {
3118
+ FunctionName: { Ref: "GreeterFunction" },
3119
+ },
3120
+ },
3121
+ GreeterAlias: {
3122
+ Type: "AWS::Lambda::Alias",
3123
+ Properties: {
3124
+ FunctionName: { Ref: "GreeterFunction" },
3125
+ Name: "live",
3126
+ FunctionVersion: { "Fn::GetAtt": ["GreeterVersion", "Version"] },
3127
+ },
3128
+ },
3129
+ },
3130
+ Outputs: {
3131
+ GreeterAliasArn: { Value: { Ref: "GreeterAlias" } },
3132
+ },
3133
+ },
3134
+ });
3135
+ await stack.waitForDeployComplete();
3136
+
3137
+ console.log(stack.output("GreeterAliasArn"));
3138
+
3139
+ const invoked = await simAws
3140
+ .lambda()
3141
+ .invoke(new InvokeCommand({ FunctionName: "greeter", Qualifier: "live" }));
3142
+
3143
+ console.log(invoked.ExecutedVersion);
3144
+
3145
+ await simAws.backgroundTasksComplete();
3146
+ ```
3147
+
3148
+ Supported `AWS::Lambda::Version` properties:
3149
+
3150
+ - `FunctionName`
3151
+ - `Description`, which describes the published version and leaves the function's own description
3152
+ alone
3153
+
3154
+ Supported `AWS::Lambda::Alias` properties:
3155
+
3156
+ - `FunctionName`
3157
+ - `Name`
3158
+ - `FunctionVersion`
3159
+ - `Description`
3160
+
3161
+ An `AWS::Lambda::Permission` naming a version or an alias grants on it. See
3162
+ [permissions in templates](#permissions-in-templates).
3163
+
3164
+ Deleting the Stack deletes the alias. Lambda has no operation that deletes one published version.
3165
+ The version Resource has nothing of its own to do on teardown, and the version it published goes
3166
+ when the function does.
3167
+
3168
+ ## Retries and destinations in templates
3169
+
3170
+ `AWS::Lambda::EventInvokeConfig` writes the event invoke config of the function, version or alias
3171
+ its `FunctionName` and `Qualifier` name. CDK emits one for `onFailure`, `onSuccess`, `retryAttempts`
3172
+ and `maxEventAge` on a function. `DeadLetterConfig` on `AWS::Lambda::Function` is what CDK's
3173
+ `deadLetterQueue` emits, and it sets the same dead-letter target `CreateFunction` takes.
3174
+
3175
+ ```typescript sim-lambda-cloudformation-event-invoke-config
3176
+ /**
3177
+ * Deploying a Lambda failure destination and a dead-letter queue from a
3178
+ * CloudFormation template.
3179
+ */
3180
+
3181
+ import { InvokeCommand } from "@aws-sdk/client-lambda";
3182
+ import { ReceiveMessageCommand } from "@aws-sdk/client-sqs";
3183
+
3184
+ import { SimAws } from "@kensio/yulin";
3185
+ import type { SimLambdaDestinationRecord } from "@kensio/yulin/lambda";
3186
+
3187
+ const simAws = new SimAws();
3188
+
3189
+ const stack = await simAws.cloudFormation().deployTemplate({
3190
+ stackName: "orders-stack",
3191
+ template: {
3192
+ Resources: {
3193
+ OrderFailures: {
3194
+ Type: "AWS::SQS::Queue",
3195
+ Properties: { QueueName: "order-failures" },
3196
+ },
3197
+ OrderDeadLetters: {
3198
+ Type: "AWS::SQS::Queue",
3199
+ Properties: { QueueName: "orders-dlq" },
3200
+ },
3201
+ OrdersFunction: {
3202
+ Type: "AWS::Lambda::Function",
3203
+ Properties: {
3204
+ FunctionName: "orders",
3205
+ Role: "arn:aws:iam::111111111111:role/OrdersRole",
3206
+ Handler: "index.handler",
3207
+ Runtime: "nodejs22.x",
3208
+ Code: {
3209
+ ZipFile:
3210
+ "exports.handler = async () => { throw new Error('failed'); };",
3211
+ },
3212
+ DeadLetterConfig: {
3213
+ TargetArn: { "Fn::GetAtt": ["OrderDeadLetters", "Arn"] },
3214
+ },
3215
+ },
3216
+ },
3217
+ OrdersInvokeConfig: {
3218
+ Type: "AWS::Lambda::EventInvokeConfig",
3219
+ Properties: {
3220
+ FunctionName: { Ref: "OrdersFunction" },
3221
+ Qualifier: "$LATEST",
3222
+ MaximumRetryAttempts: 0,
3223
+ DestinationConfig: {
3224
+ OnFailure: {
3225
+ Destination: { "Fn::GetAtt": ["OrderFailures", "Arn"] },
3226
+ },
3227
+ },
3228
+ },
3229
+ },
3230
+ },
3231
+ Outputs: {
3232
+ FailuresQueueUrl: {
3233
+ Value: { "Fn::GetAtt": ["OrderFailures", "QueueUrl"] },
3234
+ },
3235
+ },
3236
+ },
3237
+ });
3238
+ await stack.waitForDeployComplete();
3239
+
3240
+ await simAws.lambda().invoke(
3241
+ new InvokeCommand({
3242
+ FunctionName: "orders",
3243
+ InvocationType: "Event",
3244
+ Payload: JSON.stringify({ id: 7 }),
3245
+ }),
3246
+ );
3247
+ await simAws.backgroundTasksComplete();
3248
+
3249
+ const received = await simAws
3250
+ .sqs()
3251
+ .receiveMessage(
3252
+ new ReceiveMessageCommand({ QueueUrl: stack.output("FailuresQueueUrl") }),
3253
+ );
3254
+ const record = JSON.parse(
3255
+ String(received.Messages?.[0]?.Body),
3256
+ ) as SimLambdaDestinationRecord;
3257
+
3258
+ console.log(record.requestContext.condition);
3259
+ console.log(record.requestPayload);
3260
+ ```
3261
+
3262
+ Supported `AWS::Lambda::EventInvokeConfig` properties:
3263
+
3264
+ - `FunctionName`, as a `Ref` to the function giving its name or an `Fn::GetAtt` on it giving the ARN
3265
+ - `Qualifier`, naming a published version or an alias. `$LATEST` addresses the function itself
3266
+ - `MaximumRetryAttempts`
3267
+ - `MaximumEventAgeInSeconds`
3268
+ - `DestinationConfig`, holding `OnSuccess` and `OnFailure`
3269
+
3270
+ A destination and a `DeadLetterConfig.TargetArn` name a queue or a topic by `Fn::GetAtt` on its ARN
3271
+ or by `Ref`. `Ref` on an `AWS::SQS::Queue` resolves to the queue URL, and both forms reach the same
3272
+ queue.
3273
+
3274
+ Where a destination names something simulated Lambda has nowhere to send to, such as a service
3275
+ outside the template or a Resource the deployment skipped, the Resource is deployed without that
3276
+ destination and the omission is recorded in `stack.ignoredProperties` under the property that named
3277
+ it. The function still deploys, and a stack keeps the destination it can reach when the other end
3278
+ names one it cannot. Refusing a whole stack over one destination would leave a test with nothing to
3279
+ run.
3280
+
3281
+ Deleting the Stack takes the config off the function, ahead of the function itself.
3282
+
3283
+ ## Executable bindings
3284
+
3285
+ Deploy-time `bindings` let a template function be backed by a real in-process handler instead of its
3286
+ template code. They are the CloudFormation counterpart of `makeLambdaZipFileInput(...)`. The bound
3287
+ handler runs with the same execution-role attribution as template code, can close over test state,
3288
+ and can be stepped through in a debugger.
3289
+
3290
+ ```typescript sim-lambda-cloudformation-bindings
3291
+ /**
3292
+ * Binding a real in-process handler to a CloudFormation Lambda function.
3293
+ */
3294
+
3295
+ import { InvokeCommand } from "@aws-sdk/client-lambda";
3296
+
3297
+ import { SimAws } from "@kensio/yulin";
3298
+
3299
+ const simAws = new SimAws();
3300
+ const observedEvents: unknown[] = [];
3301
+
3302
+ await simAws.cloudFormation().deployTemplate({
3303
+ stackName: "bound-greeter-stack",
3304
+ template: {
3305
+ Resources: {
3306
+ GreeterFunction: {
3307
+ Type: "AWS::Lambda::Function",
3308
+ Properties: {
3309
+ FunctionName: "bound-greeter",
3310
+ Role: "arn:aws:iam::111111111111:role/BoundGreeterRole",
3311
+ },
3312
+ },
3313
+ },
3314
+ },
3315
+ bindings: [
3316
+ {
3317
+ logicalId: "GreeterFunction",
3318
+ handler: (event: { name: string }): string => {
3319
+ observedEvents.push(event);
3320
+ return `Hello ${event.name} from the bound handler`;
3321
+ },
3322
+ },
3323
+ ],
3324
+ });
3325
+
3326
+ const output = await simAws.lambda().invoke(
3327
+ new InvokeCommand({
3328
+ FunctionName: "bound-greeter",
3329
+ Payload: JSON.stringify({ name: "Yulin" }),
3330
+ }),
3331
+ );
3332
+
3333
+ if (output.Payload === undefined) throw new Error("No invoke Payload");
3334
+ console.log(Buffer.from(output.Payload).toString());
3335
+ console.log(observedEvents.length);
3336
+
3337
+ await simAws.backgroundTasksComplete();
3338
+ ```
3339
+
3340
+ A binding can target the function by `logicalId` (which also matches a CDK construct ID from
3341
+ `aws:cdk:path` metadata), by `functionName`, by `arn`, by full `cdkPath`, or by `imageRepository`
3342
+ for a function packaged as a container image. A bound function may omit template `Code` and
3343
+ `Handler` entirely, and unbound functions in the same template keep their template code on the vm
3344
+ path. A binding that resolves to no template resource fails the deploy with the unmatched target
3345
+ named for diagnosis. Where two bindings could both back the same function, the one listed first is
3346
+ the one that backs it.
3347
+
3348
+ ### Invoking a function bound by construct ID
3349
+
3350
+ A CDK function usually leaves `FunctionName` out of the template, and sim Lambda creates it under
3351
+ the synthesized logical ID. `stack.getResource` takes the construct ID the binding named. The
3352
+ Resource it answers with carries that logical ID, and it is the name to invoke. The hash CDK
3353
+ generated stays out of the test.
3354
+
3355
+ ```typescript sim-lambda-construct-id-invoke
3356
+ /**
3357
+ * Invoking a Lambda function bound by its CDK construct ID.
3358
+ */
3359
+
3360
+ import { InvokeCommand } from "@aws-sdk/client-lambda";
3361
+
3362
+ import { SimAws } from "@kensio/yulin";
3363
+
3364
+ const simAws = new SimAws();
3365
+
3366
+ const stack = await simAws.cloudFormation().deployTemplate({
3367
+ stackName: "uploads-stack",
3368
+ template: {
3369
+ Resources: {
3370
+ UploadFunction8A7B6C5D: {
3371
+ Type: "AWS::Lambda::Function",
3372
+ Metadata: {
3373
+ "aws:cdk:path": "UploadsStack/UploadFunction/Resource",
3374
+ },
3375
+ Properties: {
3376
+ Role: "arn:aws:iam::111111111111:role/UploadFunctionRole",
3377
+ },
3378
+ },
3379
+ },
3380
+ },
3381
+ bindings: [
3382
+ {
3383
+ logicalId: "UploadFunction",
3384
+ handler: (event: { key: string }): string => `stored ${event.key}`,
3385
+ },
3386
+ ],
3387
+ });
3388
+
3389
+ const upload = stack.getResource("UploadFunction");
3390
+ if (upload === undefined) throw new Error("No UploadFunction Resource");
3391
+
3392
+ const output = await simAws.lambda().invoke(
3393
+ new InvokeCommand({
3394
+ FunctionName: upload.logicalId,
3395
+ Payload: JSON.stringify({ key: "receipt.pdf" }),
3396
+ }),
3397
+ );
3398
+
3399
+ if (output.Payload === undefined) throw new Error("No invoke Payload");
3400
+ console.log(Buffer.from(output.Payload).toString());
3401
+ // "stored receipt.pdf"
3402
+
3403
+ await simAws.backgroundTasksComplete();
3404
+ ```
3405
+
3406
+ A function with a `FunctionName` in the template is invoked by that name. The caller wrote it and
3407
+ already holds it.
3408
+
3409
+ ### Binding by container image repository
3410
+
3411
+ `imageRepository` matches any function whose resolved `Code.ImageUri` names that repository. One
3412
+ binding covers every function running that image, in every stack deployed from the same `SimAws`.
3413
+ A logical ID belongs to one construct tree.
3414
+
3415
+ The image tag is ignored on both sides of the match. No tag is stable enough to write into a test.
3416
+ A CDK image asset is tagged with the asset content hash, which changes whenever the image source
3417
+ does, and a pipeline-built image is usually tagged with a git sha or a build number passed in as a
3418
+ stack parameter. The registry host is part of the repository, so the account and region have to
3419
+ match too, and an `ImageUri` built by `Fn::Sub` or from a stack parameter is matched on what it
3420
+ resolves to.
3421
+
3422
+ ```typescript sim-lambda-image-repository-binding
3423
+ /**
3424
+ * Binding a handler to a container image function by its image repository.
3425
+ */
3426
+
3427
+ import { InvokeCommand } from "@aws-sdk/client-lambda";
3428
+
3429
+ import { SimAws } from "@kensio/yulin";
3430
+
3431
+ const simAws = new SimAws();
3432
+
3433
+ await simAws.cloudFormation().deployTemplate({
3434
+ stackName: "orders-stack",
3435
+ template: {
3436
+ Parameters: {
3437
+ ImageTag: { Type: "String" },
3438
+ },
3439
+ Resources: {
3440
+ OrdersFunction: {
3441
+ Type: "AWS::Lambda::Function",
3442
+ Properties: {
3443
+ FunctionName: "orders",
3444
+ Role: "arn:aws:iam::111111111111:role/OrdersRole",
3445
+ PackageType: "Image",
3446
+ Code: {
3447
+ ImageUri: {
3448
+ "Fn::Sub":
3449
+ // eslint-disable-next-line no-template-curly-in-string
3450
+ "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/orders:${ImageTag}",
3451
+ },
3452
+ },
3453
+ },
3454
+ },
3455
+ },
3456
+ },
3457
+ parameters: { ImageTag: "build-4172" },
3458
+ bindings: [
3459
+ {
3460
+ imageRepository:
3461
+ `${simAws.defaultAccountId}.dkr.ecr.` +
3462
+ `${simAws.defaultRegionName}.amazonaws.com/orders`,
3463
+ handler: (event: { orderId: string }): string =>
3464
+ `Processed ${event.orderId}`,
3465
+ },
3466
+ ],
3467
+ });
3468
+
3469
+ const output = await simAws.lambda().invoke(
3470
+ new InvokeCommand({
3471
+ FunctionName: "orders",
3472
+ Payload: JSON.stringify({ orderId: "order-1" }),
3473
+ }),
3474
+ );
3475
+
3476
+ if (output.Payload === undefined) throw new Error("No invoke Payload");
3477
+ console.log(Buffer.from(output.Payload).toString());
3478
+
3479
+ await simAws.backgroundTasksComplete();
3480
+ ```
3481
+
3482
+ A function matched this way is created from the bound handler, and never reaches the
3483
+ [container image skip](#container-image-functions). Functions in the same template running an image
3484
+ from another repository are skipped as usual.
3485
+
3486
+ A binding like this and a handler registered in
3487
+ [simulated ECR](https://yulinsim.dev/services/ecr/ "Simulated ECR usage docs") match on the same thing, and the binding is what
3488
+ backs the function where both could. Reach for the binding when the handler belongs to one deploy,
3489
+ and for the repository when it belongs to the image.
3490
+
3491
+ ## Available functionality
3492
+
3493
+ Sim Lambda currently supports:
3494
+
3495
+ - `CreateFunctionCommand` and `GetFunctionCommand`, including a function created from the
3496
+ [simulated ECR](https://yulinsim.dev/services/ecr/ "Simulated ECR usage docs") image its `Code.ImageUri` names
3497
+ - `UpdateFunctionCodeCommand`, replacing the code `$LATEST` runs while the function keeps its
3498
+ policy, Function URL, versions and aliases
3499
+ - `UpdateFunctionConfigurationCommand`, changing the `Role`, `Handler`, `Runtime`, `Description`,
3500
+ `Timeout`, `MemorySize` and `Environment` a function runs with
3501
+ - `ListFunctionsCommand`, reporting every function in the Account and Region, and their published
3502
+ versions with `FunctionVersion: "ALL"`
3503
+ - `InvokeCommand`, with the `RequestResponse`, `Event` and `DryRun` invocation types
3504
+ - Asynchronous invocation retries on the simulated clock, and `OnSuccess`/`OnFailure` destinations
3505
+ written with `PutFunctionEventInvokeConfigCommand`, delivering the AWS destination record to a
3506
+ simulated SQS queue, SNS topic, EventBridge event bus or Lambda function
3507
+ - `GetFunctionEventInvokeConfigCommand`, `UpdateFunctionEventInvokeConfigCommand`,
3508
+ `DeleteFunctionEventInvokeConfigCommand` and `ListFunctionEventInvokeConfigsCommand`, each taking
3509
+ a `Qualifier` for a version's or an alias's own config
3510
+ - `DeadLetterConfig` on `CreateFunction` and `UpdateFunctionConfiguration`, sending an abandoned
3511
+ event to a simulated SQS queue or SNS topic
3512
+ - Function URLs, created with `CreateFunctionUrlConfigCommand` and served over HTTP on localhost
3513
+ with `serveSimAws`
3514
+ - `iam:PassRole` authorization of the execution role named by `CreateFunctionCommand` and
3515
+ `UpdateFunctionConfigurationCommand`, with `iam:PassedToService` supplied
3516
+ - `AuthType: "AWS_IAM"` Function URLs, authorizing `lambda:InvokeFunctionUrl` against the caller
3517
+ resolved from the request, and `lambda:InvokeFunction` as well for a CloudFront origin access
3518
+ control
3519
+ - `lambdaFunctionUrlEventFactory`, making a Function URL invocation event for a test that calls a
3520
+ handler directly
3521
+ - `AddPermissionCommand`, `RemovePermissionCommand` and `GetPolicyCommand`, for resource-based
3522
+ policies evaluated alongside identity policies
3523
+ - SQS, DynamoDB stream and Kinesis stream event source mappings, created with
3524
+ `CreateEventSourceMappingCommand` and read with `GetEventSourceMappingCommand`,
3525
+ `ListEventSourceMappingsCommand` and `DeleteEventSourceMappingCommand`, delivering real-shaped SQS,
3526
+ DynamoDB stream and Kinesis events and honouring `BatchSize`
3527
+ - `StartingPosition: "TRIM_HORIZON"` and `"LATEST"` on a stream mapping, and `"AT_TIMESTAMP"` on a
3528
+ Kinesis one, with a failing batch blocking its shard until it is through or discarded
3529
+ - Every shard of a Kinesis stream read by a processor of its own, as real Lambda reads one
3530
+ - `FunctionResponseTypes: ["ReportBatchItemFailures"]` on a queue mapping, returning only the
3531
+ message ids the handler reported, and on a stream mapping, rewinding to the lowest sequence number
3532
+ the handler reported
3533
+ - Function code from three sources:
3534
+ - an in-process handler function passed via `makeLambdaZipFileInput(...)`
3535
+ - zip archive bytes on `Code.ZipFile` (build them with `makeLambdaCodeZip(...)`)
3536
+ - a zip object stored in sim S3 via `Code.S3Bucket`/`S3Key`
3537
+ - A Node.js `vm` runtime for zip-packaged code, with warm module state across invocations, and
3538
+ writable standard streams for handler output, including a bundled AWS Lambda Powertools logger's
3539
+ own console
3540
+ - Per-function environment variables with `Environment.Variables`
3541
+ - Runtime-provided `@aws-sdk/*` packages inside function code, routed into the owning simulated AWS
3542
+ environment
3543
+ - `fetch`, `node:http` and `node:https` requests from function code, answered by the simulation for
3544
+ every hostname it serves, including a Cognito user pool domain's OAuth endpoints and the JWKS a
3545
+ token verifier fetches from the regional Cognito endpoint
3546
+ - Execution roles, evaluated against simulated IAM
3547
+ - Published versions and aliases, with `PublishVersion`, `ListVersionsByFunction`, `CreateAlias`,
3548
+ `UpdateAlias`, `GetAlias`, `ListAliases` and `DeleteAlias`, and a `Qualifier` on `Invoke`,
3549
+ `GetFunction` and the permission commands, each qualified resource holding its own policy
3550
+ - IAM authorization of the Lambda commands themselves (`lambda:CreateFunction`,
3551
+ `lambda:GetFunction`, `lambda:UpdateFunctionCode`, `lambda:InvokeFunction`, the Function URL
3552
+ config actions, and the version and alias actions), and `lambda:ListFunctions` on `*` for the
3553
+ listing
3554
+ - AWS-like validation and errors, such as `ResourceConflictException` for a duplicate function name
3555
+ - The `AWS::Lambda::Function`, `AWS::Lambda::Url`, `AWS::Lambda::Permission`,
3556
+ `AWS::Lambda::Version`, `AWS::Lambda::Alias`, `AWS::Lambda::EventSourceMapping` and
3557
+ `AWS::Lambda::EventInvokeConfig` CloudFormation resources, with `Ref`/`Fn::GetAtt` support and
3558
+ deploy-time executable bindings
3559
+ - A CDK app built on `fn.currentVersion` or a `lambda.Alias`, which deploys with the alias its
3560
+ integrations point at, and where an `AWS::Lambda::Permission` naming that alias grants on the
3561
+ alias
3562
+ - `AWS::Lambda::EventSourceMapping` on a queue, on a table's stream or on a Kinesis stream,
3563
+ including the `StartingPosition` a stream mapping needs, so a CDK `SqsEventSource`,
3564
+ `DynamoEventSource` or `KinesisEventSource` deploys as it is synthesised
3565
+ - A CDK function given `onFailure`, `onSuccess`, `retryAttempts`, `maxEventAge` or
3566
+ `deadLetterQueue`, which deploys the `AWS::Lambda::EventInvokeConfig` and the `DeadLetterConfig`
3567
+ those synthesise
3568
+
3569
+ ## Limitations
3570
+
3571
+ Current documented limitations:
3572
+
3573
+ - Only `CreateFunctionCommand`, `GetFunctionCommand`, `UpdateFunctionCodeCommand`,
3574
+ `UpdateFunctionConfigurationCommand`, `ListFunctionsCommand`, `DeleteFunctionCommand`,
3575
+ `InvokeCommand`, the permission commands (`AddPermissionCommand`, `RemovePermissionCommand`,
3576
+ `GetPolicyCommand`), the version and alias commands, the Function URL config commands and the
3577
+ event source mapping commands are supported. `GetFunctionConfiguration` and the concurrency and
3578
+ tagging commands are absent so far.
3579
+ - `UpdateFunctionCode` leaves out the `RevisionId` and `DryRun` preconditions real Lambda takes,
3580
+ along with `Architectures` and `SourceKMSKeyArn`. `UpdateFunctionConfiguration` takes only the
3581
+ settings simulated Lambda models, leaving out `Layers`, `VpcConfig`, `TracingConfig`, `KMSKeyArn`,
3582
+ `EphemeralStorage`, `SnapStart`, `LoggingConfig` and `RevisionId`. `ListFunctions` leaves out
3583
+ `Marker`/`MaxItems` paging and `MasterRegion`.
3584
+ - A destination is delivered to without checking that the function's execution role may write to it.
3585
+ Real Lambda delivers under that role and drops the record where the role cannot, which would be
3586
+ silent, so simulated Lambda delivers instead. Nothing else about the destination has to be set up,
3587
+ since a destination needs no resource policy on real AWS either.
3588
+ - SAM's `EventInvokeConfig` and `DeadLetterQueue` on `AWS::Serverless::Function` are left out. A
3589
+ SAM application declaring either deploys a function with the default retries, no destinations and
3590
+ no dead-letter target. The `AWS::Lambda::EventInvokeConfig` Resource and the `DeadLetterConfig`
3591
+ property a CloudFormation or CDK template writes are both deployed. See
3592
+ [retries and destinations in templates](#retries-and-destinations-in-templates).
3593
+ - Throttling and timeouts do not drive a retry. A retry follows a handler that threw, and the
3594
+ `MaximumEventAgeInSeconds` a config carries is measured from when the invocation was accepted.
3595
+ - `DestinationConfig` on an event source mapping is left out. It is a different mechanism from the
3596
+ asynchronous invocation destinations above.
3597
+ - A settings change takes effect at once. Real Lambda reports `LastUpdateStatus: "InProgress"` while
3598
+ it rolls the change out, and neither that member nor the wait it implies is simulated.
3599
+ - A cross-account grant is only half of what admits a call. The caller's own Account has to allow
3600
+ the action too, and its IAM has to be part of the same `SimAws` instance for its policies to be
3601
+ found. A caller from an Account outside the simulation is denied.
3602
+ - `lambda:FunctionUrlAuthType`, `AWS:SourceArn` and `AWS:SourceAccount` are the only condition keys
3603
+ given a value at request time. The first is supplied when a Function URL is invoked, and the other
3604
+ two when another simulated service invokes the function on a resource's behalf. See
3605
+ [Resource-based policies](#resource-based-policies) for which paths those are.
3606
+ `PrincipalOrgID` and `InvokedViaFunctionUrl` are written into the statement so `GetPolicy` reports
3607
+ the grant that was made, and no value is supplied for them, so a statement carrying one never
3608
+ matches.
3609
+ - `RevisionId` and `EventSourceToken` on the permission commands are left out.
3610
+ - `requestContext.authorizer.iam` reports `accessKey` as empty, and `callerId` and `userId` as the
3611
+ caller ARN rather than the opaque unique id real AWS uses. `cognitoIdentity` and `principalOrgId`
3612
+ are always null.
3613
+ - The Function URL `Cors` configuration is left out, including OPTIONS preflight handling.
3614
+ - `InvokeMode: "RESPONSE_STREAM"` is accepted and reported, but responses are always served
3615
+ buffered.
3616
+ - A function has at most one Function URL, and qualified (version or alias) Function URLs are left
3617
+ out.
3618
+ - A published version keeps what it was published with, so `UpdateFunctionCode` moves `$LATEST`
3619
+ alone. The `CodeSha256` and `RevisionId` checks `PublishVersion` makes are left out, along with
3620
+ alias `RoutingConfig` weights, provisioned concurrency, and the `Marker`/`MaxItems` paging on the
3621
+ two listings. `CodeSha256`, `RuntimePolicy`
3622
+ and `ProvisionedConcurrencyConfig` on `AWS::Lambda::Version`, and `RoutingConfig` and
3623
+ `ProvisionedConcurrencyConfig` on `AWS::Lambda::Alias`, are accepted and ignored. SAM's
3624
+ `AutoPublishAlias` is left out.
3625
+ - A qualified function ARN reaches a function through S3 notifications, SNS subscriptions,
3626
+ CloudWatch Logs subscriptions, Cognito triggers, EventBridge targets, event source mappings and
3627
+ API Gateway integration and authorizer URIs, and `AWS::Lambda::Permission`. The
3628
+ `AWS::Lambda::Url` and `AWS::Lambda::EventSourceMapping` template readers still refuse or drop a
3629
+ qualifier.
3630
+ - The vm runtime supports CommonJS function code only. An ES module deployment package is refused
3631
+ at cold start, whether the handler file ends in `.mjs` or a `.js` file opens on `import`.
3632
+ Evaluating one would need `vm.SourceTextModule`, which Node.js gates behind
3633
+ `--experimental-vm-modules` at process launch, and a simulator that made every consumer pass a
3634
+ Node.js flag would cost more than it repaid. Back the function with an in-process handler, or
3635
+ deploy a CommonJS build of the same source. See
3636
+ [zip-packaged code and the vm runtime](#zip-packaged-code-and-the-vm-runtime).
3637
+ - A handler function reference is recorded through the process console and the process standard
3638
+ streams, both of which a test runner is free to replace. `console.trace` and `console.dir`
3639
+ decorate what they print, and either one reaches the log group only where the host console passes
3640
+ it on to `process.stdout`. See [What a handler prints](#what-a-handler-prints).
3641
+ - The platform `START`, `END` and `REPORT` lines a real log stream carries are left out, and
3642
+ execution environments are never recycled, so a function keeps one log stream for as long as it
3643
+ exists.
3644
+ - Container image functions are never run. Yulin never reads a container image, and stays
3645
+ Docker-free.
3646
+ A function with `PackageType: Image` is skipped, or refused on `CreateFunction`, unless a real
3647
+ in-process handler stands in for its image, either one bound to it or one registered in the
3648
+ [simulated ECR](https://yulinsim.dev/services/ecr/ "Simulated ECR usage docs") repository the image URI names. See
3649
+ [Container image functions](#container-image-functions).
3650
+ - Lambda Layers are left out.
3651
+ - Environment variables declared with `Environment.Variables` reach a real in-process handler
3652
+ function only while it runs, so a variable read at module scope sees the host process value
3653
+ instead. See [Environment variables](#environment-variables).
3654
+ - Outbound HTTP is routed for `fetch`, `node:http` and `node:https` alone. A handler using another
3655
+ client, or a request made from a handler module while it is being imported, reaches the network.
3656
+ A routed response comes back as the simulation answered it, with redirects left for the caller to
3657
+ follow. See [The HTTP requests function code makes](#the-http-requests-function-code-makes).
3658
+ - The AWS service API endpoints answered as Commands are the ones a request can be read back from,
3659
+ and the hostnames answered over HTTP are the ones simulated Route53 resolves. The endpoints AWS
3660
+ issues for one resource, a Lambda Function URL and an API Gateway HTTP API, resolve under the
3661
+ hostnames the simulator serves them on. A request to the AWS form of one of those reaches the
3662
+ network.
3663
+ - An unsigned request to a service API endpoint is served over HTTP when the simulation serves that
3664
+ endpoint, which today means Cognito's regional endpoint and the S3 endpoints. Anywhere else it is
3665
+ read as a Command and refused as one.
3666
+ - `Timeout` is recorded and never interrupts handler execution.
3667
+ - Timers inside a handler are host timers. `setTimeout` waits in real time, and advancing the
3668
+ simulation's clock leaves a sleeping handler asleep.
3669
+ - A time read at module scope, like an environment variable read there, is read before any
3670
+ invocation and sees the host clock. See [The time inside a handler](#the-time-inside-a-handler).
3671
+ - `Event` invocation retries and failure destinations are left out, and handler errors are dropped.
3672
+ - The S3 object version a code location names is accepted but ignored, as sim S3 has no object
3673
+ versioning yet. That covers `Code.S3ObjectVersion` on `CreateFunction` and on a template
3674
+ function, and `S3ObjectVersion` on `UpdateFunctionCode`. A versioned location loads the object as
3675
+ it stands.
3676
+ - SQS queues, DynamoDB streams and Kinesis streams are the only event sources. Kafka, DocumentDB and
3677
+ Kinesis enhanced fan-out consumers are refused outright, and so are `FilterCriteria`,
3678
+ `ScalingConfig`, `DestinationConfig`, `MaximumRetryAttempts`, `BisectBatchOnFunctionError`,
3679
+ `ParallelizationFactor`, `TumblingWindowInSeconds` and the other mapping inputs this simulation
3680
+ has no behaviour for.
3681
+ - A stream batch is delivered again five times, after 1, 2, 4, 8 and 16 seconds, and then discarded.
3682
+ AWS documents no delay between attempts and retries until the records age out. Both differences
3683
+ are deliberate. A delay of zero falls due at the instant the clock already reads, and a handler
3684
+ that always throws would leave `advanceBy` with work falling due forever. A batch item failure
3685
+ report counts against the same five attempts, and never starts them again for the records it
3686
+ rewound to.
3687
+ - A handler writing into the table whose stream invoked it is refused with
3688
+ `SimLambdaStreamCascadeError` rather than being delivered its own writes forever. Real Lambda runs
3689
+ that loop.
3690
+ - A shard iterator never expires, where a real one is good for 15 minutes.
3691
+ - `MaximumBatchingWindowInSeconds` is only simulated as 0. A partial batch is delivered as soon as
3692
+ anything is on the event source, leaving a batching window nothing to wait for. A non-zero value
3693
+ is refused, which also caps a queue mapping's `BatchSize` at 10. The documented rule that a
3694
+ `BatchSize` above 10 needs a batching window goes unenforced, because the same AWS documentation
3695
+ gives a stream mapping `BatchSize` 100 and window 0 as simultaneous defaults, and CDK emits
3696
+ exactly that.
3697
+ - An execution role that gets its stream permissions from the AWS managed policy
3698
+ `AWSLambdaDynamoDBExecutionRole` has none here, since simulated IAM has no model for managed
3699
+ policy ARNs, so the mapping is refused when it is created. A hand-written template and a SAM application
3700
+ usually attach that policy where CDK writes an inline one, so the two declaration paths differ.
3701
+ Write the grant inline to deploy either.
3702
+ - `UpdateEventSourceMapping` is absent, and a mapping's batch size or enabled state is fixed once it
3703
+ is created. `Enabled: false` at creation is simulated.
3704
+ - One poll of one shard delivers one batch. Real Lambda runs several pollers at once and scales them
3705
+ with the event source, and what that concurrency does to ordering or to a downstream service is
3706
+ invisible here. A simulated DynamoDB stream has one shard and never splits, so a mapping on one has
3707
+ a single thing to read either way. A Kinesis stream has the shards it was created with, each read
3708
+ by a processor of its own, and none of them ever splits or merges.
3709
+ - CloudFormation resource types other than `AWS::Lambda::Function`, `AWS::Lambda::Url`,
3710
+ `AWS::Lambda::Permission`, `AWS::Lambda::Version`, `AWS::Lambda::Alias`,
3711
+ `AWS::Lambda::EventSourceMapping` and `AWS::Lambda::EventInvokeConfig` (`LayerVersion`,
3712
+ `CodeSigningConfig`, ...) are skipped with an "Unsupported" diagnostic.
3713
+ - The `vm` context is a namespacing convenience rather than a security boundary. Function code runs
3714
+ in-process with the same trust as the test suite itself. Do not run untrusted code through the
3715
+ simulator.
3716
+ - `serveSimAws` serves nineteen of these operations over HTTP, listed in the
3717
+ [serving docs](https://yulinsim.dev/serve/#lambda-over-the-endpoint). The version and alias operations have no
3718
+ route there yet, and reach the simulation through `SimAws` or
3719
+ [SDK interception](https://yulinsim.dev/sdk/) instead.