@kensio/yulin 1.20.14 → 1.20.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +13 -0
  2. package/{dist/config/oxlint/cffjs2.oxlintrc.json → cffjs2.oxlintrc.json} +1 -1
  3. package/dist/config/oxlint/cffjs2.oxlint.config.js +4 -3
  4. package/dist/sdk/module/sim-sdk-module-client-interceptor.d.ts +5 -0
  5. package/dist/sdk/module/sim-sdk-module-client-interceptor.js +14 -0
  6. package/dist/sdk/module/sim-sdk-static-client-factory.d.ts +26 -0
  7. package/dist/sdk/module/sim-sdk-static-client-factory.js +59 -0
  8. package/dist/sdk/send-patch.d.ts +8 -0
  9. package/dist/sdk/send-patch.js +11 -0
  10. package/dist/service/aws/caller/sim-aws-caller-resolver.d.ts +28 -6
  11. package/dist/service/aws/caller/sim-aws-caller-resolver.js +18 -11
  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 +6 -0
  16. package/dist/service/aws/factory/sim-aws-account-region-service-builder.js +3 -0
  17. package/dist/service/aws/factory/sim-aws-account-service-cache.d.ts +6 -0
  18. package/dist/service/aws/factory/sim-aws-account-service-cache.js +5 -0
  19. package/dist/service/aws/factory/sim-aws-service-factory.d.ts +5 -0
  20. package/dist/service/aws/factory/sim-aws-service-factory.js +6 -0
  21. package/dist/service/aws/sim-aws-properties.d.ts +15 -0
  22. package/dist/service/aws/sim-aws.js +2 -1
  23. package/dist/service/cloudformation/resource/resolve/property/sim-cfn-resource-property-resolver.d.ts +11 -4
  24. package/dist/service/cloudformation/resource/resolve/property/sim-cfn-resource-property-resolver.js +21 -8
  25. package/dist/service/cloudformation/resource/sim-cfn-resource.type.d.ts +9 -0
  26. package/dist/service/cloudformation/template/dynamic/make-sim-cfn-dynamic-references.d.ts +2 -0
  27. package/dist/service/cloudformation/template/dynamic/make-sim-cfn-dynamic-references.js +2 -2
  28. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-resolvers.d.ts +10 -0
  29. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-resolvers.js +4 -3
  30. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-scan.d.ts +11 -0
  31. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-scan.js +21 -0
  32. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-references.d.ts +24 -2
  33. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-references.js +50 -11
  34. package/dist/service/cloudformation/template/dynamic/sim-cfn-prefetched-dynamic-references.d.ts +42 -0
  35. package/dist/service/cloudformation/template/dynamic/sim-cfn-prefetched-dynamic-references.js +80 -0
  36. package/dist/service/eventbridge/delivery/sim-event-bridge-delivery-failures.d.ts +16 -0
  37. package/dist/service/eventbridge/delivery/sim-event-bridge-delivery-failures.js +19 -0
  38. package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.d.ts +30 -7
  39. package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.js +20 -11
  40. package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.d.ts +2 -1
  41. package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.js +5 -1
  42. package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.d.ts +10 -0
  43. package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.js +15 -3
  44. package/dist/service/iam/authorize/sim-iam-account-auth-z.d.ts +11 -4
  45. package/dist/service/iam/authorize/sim-iam-account-auth-z.js +5 -3
  46. package/dist/service/iam/authorize/sim-iam-authorizer.d.ts +2 -1
  47. package/dist/service/iam/authorize/sim-iam-authorizer.js +1 -0
  48. package/dist/service/iam/command/policy/create-policy/create-policy-input-resolver.js +4 -1
  49. package/dist/service/iam/command/policy/put-role-policy/put-role-policy.handler.js +5 -1
  50. package/dist/service/iam/command/policy/put-user-policy/put-user-policy.handler.js +5 -1
  51. package/dist/service/iam/policy/parse/sim-iam-document-parser.d.ts +4 -1
  52. package/dist/service/iam/policy/parse/sim-iam-document-parser.js +13 -15
  53. package/dist/service/iam/policy/sim-iam-statement-label.d.ts +28 -0
  54. package/dist/service/iam/policy/sim-iam-statement-label.js +17 -0
  55. package/dist/service/iam/policy/sim-iam-statement-strings.d.ts +11 -0
  56. package/dist/service/iam/policy/sim-iam-statement-strings.js +27 -0
  57. package/dist/service/iam/role/sim-iam-role-with-policy.factory.d.ts +9 -0
  58. package/dist/service/iam/role/sim-iam-role-with-policy.factory.js +4 -2
  59. package/dist/service/iam/sim-iam-account-parts.d.ts +8 -0
  60. package/dist/service/iam/sim-iam-account-parts.js +1 -0
  61. package/dist/service/iam/validate/sim-iam-policy-document-validator.d.ts +9 -2
  62. package/dist/service/iam/validate/sim-iam-policy-document-validator.js +37 -19
  63. package/dist/service/lambda/function/code/sim-lambda-vm-zip-function.factory.d.ts +7 -0
  64. package/dist/service/lambda/function/code/sim-lambda-vm-zip-function.factory.js +1 -0
  65. package/dist/service/lambda/function/code/vm/sdk/sim-lambda-provided-sdk-module.d.ts +19 -0
  66. package/dist/service/lambda/function/code/vm/sdk/sim-lambda-provided-sdk-module.js +78 -0
  67. package/dist/service/lambda/function/code/vm/sdk/sim-lambda-sdk-packages-not-installed.error.d.ts +15 -0
  68. package/dist/service/lambda/function/code/vm/sdk/sim-lambda-sdk-packages-not-installed.error.js +27 -0
  69. package/dist/service/lambda/function/code/vm/sdk/sim-lambda-vm-sdk-module-provider.d.ts +9 -0
  70. package/dist/service/lambda/function/code/vm/sdk/sim-sdk-lambda-vm-module-provider.d.ts +10 -0
  71. package/dist/service/lambda/function/code/vm/sdk/sim-sdk-lambda-vm-module-provider.js +28 -34
  72. package/dist/service/lambda/function/code/vm/sim-lambda-host-modules.d.ts +15 -0
  73. package/dist/service/lambda/function/code/vm/sim-lambda-host-modules.js +31 -0
  74. package/dist/service/lambda/function/code/vm/sim-lambda-vm-module-resolver.d.ts +7 -0
  75. package/dist/service/lambda/function/code/vm/sim-lambda-vm-module-resolver.js +15 -0
  76. package/dist/service/lambda/function/code/vm/sim-lambda-vm-modules.js +5 -4
  77. package/dist/service/s3/command/put-bucket-policy/put-bucket-policy.handler.js +4 -1
  78. package/dist/service/scheduler/delivery/sim-scheduler-delivery-failures.d.ts +16 -0
  79. package/dist/service/scheduler/delivery/sim-scheduler-delivery-failures.js +19 -0
  80. package/dist/service/scheduler/index.d.ts +1 -1
  81. package/dist/service/scheduler/index.js +1 -1
  82. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-dynamic-reference-resolver.d.ts +13 -15
  83. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-dynamic-reference-resolver.js +22 -43
  84. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-stand-in.d.ts +9 -0
  85. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-stand-in.js +14 -0
  86. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-value.d.ts +12 -0
  87. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-value.js +29 -0
  88. package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-dynamic-reference-resolver.d.ts +22 -6
  89. package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-dynamic-reference-resolver.js +39 -24
  90. package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-secure-dynamic-reference-resolver.d.ts +10 -5
  91. package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-secure-dynamic-reference-resolver.js +10 -8
  92. package/dist/service/ssm/sim-ssm.d.ts +10 -2
  93. package/dist/service/ssm/sim-ssm.js +12 -4
  94. package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.d.ts +9 -2
  95. package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.js +2 -2
  96. package/dist/service/sts/auth-z/assume-role-source-account-auth-z.d.ts +10 -4
  97. package/dist/service/sts/auth-z/assume-role-source-account-auth-z.js +8 -7
  98. package/dist/service/sts/auth-z/assume-role-target-auth-z.d.ts +2 -2
  99. package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.d.ts +6 -2
  100. package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.js +14 -11
  101. package/dist/service/sts/command/assume-role/assume-role.handler.d.ts +5 -1
  102. package/dist/service/sts/command/assume-role/assume-role.handler.js +5 -2
  103. package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.d.ts +5 -1
  104. package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.js +4 -1
  105. package/dist/service/sts/service-role/sim-service-role.js +5 -1
  106. package/dist/service/sts/sim-sts.d.ts +9 -1
  107. package/dist/service/sts/sim-sts.js +4 -0
  108. package/docs/README.md +54 -0
  109. package/docs/ai-skill/README.md +69 -0
  110. package/docs/cli/README.md +284 -0
  111. package/docs/factories/README.md +140 -0
  112. package/docs/lint/README.md +176 -0
  113. package/docs/non-aws-dependencies/README.md +261 -0
  114. package/docs/sdk/README.md +272 -0
  115. package/docs/serve/README.md +1126 -0
  116. package/docs/services/acm/README.md +823 -0
  117. package/docs/services/apigateway/README.md +1919 -0
  118. package/docs/services/apigatewayv2/README.md +2678 -0
  119. package/docs/services/athena/README.md +1044 -0
  120. package/docs/services/bedrock/README.md +350 -0
  121. package/docs/services/cloudformation/README.md +3440 -0
  122. package/docs/services/cloudfront/README.md +2874 -0
  123. package/docs/services/cloudwatch/README.md +420 -0
  124. package/docs/services/cognito/README.md +4923 -0
  125. package/docs/services/dynamodb/README.md +3784 -0
  126. package/docs/services/ecr/README.md +302 -0
  127. package/docs/services/ecs/README.md +2010 -0
  128. package/docs/services/elbv2/README.md +1897 -0
  129. package/docs/services/eventbridge/README.md +1110 -0
  130. package/docs/services/firehose/README.md +805 -0
  131. package/docs/services/glue/README.md +501 -0
  132. package/docs/services/iam/README.md +1283 -0
  133. package/docs/services/kinesis/README.md +454 -0
  134. package/docs/services/kms/README.md +659 -0
  135. package/docs/services/lambda/README.md +3645 -0
  136. package/docs/services/logs/README.md +868 -0
  137. package/docs/services/organizations/README.md +574 -0
  138. package/docs/services/personalize/README.md +835 -0
  139. package/docs/services/rekognition/README.md +1070 -0
  140. package/docs/services/route53/README.md +1783 -0
  141. package/docs/services/s3/README.md +3024 -0
  142. package/docs/services/scheduler/README.md +806 -0
  143. package/docs/services/secretsmanager/README.md +667 -0
  144. package/docs/services/ses/README.md +1011 -0
  145. package/docs/services/sns/README.md +1904 -0
  146. package/docs/services/sqs/README.md +1185 -0
  147. package/docs/services/ssm/README.md +1072 -0
  148. package/docs/services/stepfunctions/README.md +1332 -0
  149. package/docs/services/sts/README.md +245 -0
  150. package/docs/services/wafv2/README.md +1378 -0
  151. package/docs/terraform/README.md +248 -0
  152. package/docs/time/README.md +284 -0
  153. package/llms.txt +59 -0
  154. package/package.json +9 -5
@@ -0,0 +1,1126 @@
1
+ # Localhost server
2
+
3
+ Serving puts a simulated AWS environment behind a real port. A browser, `curl` or an SDK client then
4
+ reaches it over HTTP. The same request path is also available in the process with no port at all,
5
+ which is the form a test usually wants.
6
+
7
+ ## Serving a port
8
+
9
+ `serveSimAws` starts the server and returns once it is listening:
10
+
11
+ ```typescript sim-serve-localhost
12
+ /**
13
+ * Serving a simulated environment on a port of your choosing.
14
+ */
15
+
16
+ import { SimAws } from "@kensio/yulin";
17
+ import { serveSimAws } from "@kensio/yulin/serve";
18
+
19
+ const simAws = new SimAws();
20
+ const srv = await serveSimAws({ simAws, port: 8787 });
21
+
22
+ console.log(srv.port); // "8787"
23
+
24
+ await srv.close();
25
+ ```
26
+
27
+ Without a `port` the server takes whatever port is free, which changes on every run. Pin one when
28
+ the URL needs to stay the same, such as when a browser is pointed at it.
29
+
30
+ A URL from a simulated service points at a simulated hostname on its usual port, and needs adapting
31
+ before it can be fetched. `srv.localUrl(...)` swaps in the port the server took:
32
+
33
+ ```typescript sim-serve-local-url
34
+ /**
35
+ * Turning a simulated AWS URL into one that reaches the local server.
36
+ */
37
+
38
+ import { CreateBucketCommand } from "@aws-sdk/client-s3";
39
+ import { SimAws } from "@kensio/yulin";
40
+ import { serveSimAws } from "@kensio/yulin/serve";
41
+
42
+ const simAws = new SimAws();
43
+ const srv = await serveSimAws({ simAws });
44
+
45
+ await simAws.s3().createBucket(new CreateBucketCommand({ Bucket: "foo-site" }));
46
+
47
+ const websiteUrl = simAws.s3().getBucketWebsiteUrl("foo-site");
48
+ console.log(srv.localUrl(websiteUrl).toString());
49
+ // http://foo-site.s3-website.us-east-1.sim-aws.localhost:<srv.port>/
50
+ // with whatever port this run took, since none was pinned.
51
+
52
+ await srv.close();
53
+ ```
54
+
55
+ ### Redirects
56
+
57
+ A simulated service redirects to the hostname it would use against real AWS. Served on localhost that
58
+ address reaches the public internet, and a browser following it leaves the simulation behind.
59
+
60
+ A `Location` header naming a hostname the simulation serves is put into its localhost form on the way
61
+ out of the local server. A sign-in page behind `www.example.com` redirecting to its user pool domain
62
+ answers with an address the browser can open:
63
+
64
+ ```http
65
+ 303 See Other
66
+ Location: http://auth.example.com.sim-aws.localhost:8787/oauth2/authorize?client_id=1a2b3c
67
+ ```
68
+
69
+ The rewrite is `srv.localUrl(...)` applied to the header, and the hostname is matched the way an
70
+ arriving request's hostname is matched. A `Location` naming an address the simulation serves nothing
71
+ at is passed on as the service wrote it. So is a relative one, since that is already relative to the
72
+ address the client arrived at.
73
+
74
+ Only the local server does this. `SimAwsHttp` reaches every simulated hostname by its own name. A
75
+ test using it sees the `Location` the service issued and asserts on the production hostname.
76
+
77
+ ## Requests without a port
78
+
79
+ `SimAwsHttp` is the same request path with no socket under it. It takes a Fetch API `Request` and
80
+ answers with a `Response`, in the process that built the environment:
81
+
82
+ ```typescript sim-serve-in-process-request
83
+ /**
84
+ * Requesting a simulated S3 website with no server listening.
85
+ */
86
+
87
+ import {
88
+ CreateBucketCommand,
89
+ PutBucketPolicyCommand,
90
+ PutBucketWebsiteCommand,
91
+ PutObjectCommand,
92
+ PutPublicAccessBlockCommand,
93
+ } from "@aws-sdk/client-s3";
94
+ import { SimAws } from "@kensio/yulin";
95
+ import { SimAwsHttp } from "@kensio/yulin/serve";
96
+
97
+ const simAws = new SimAws();
98
+ const simAwsHttp = new SimAwsHttp({ simAws });
99
+ const simS3 = simAws.region("eu-west-2").s3();
100
+
101
+ await simS3.createBucket(new CreateBucketCommand({ Bucket: "foo-site" }));
102
+ await simS3.putObject(
103
+ new PutObjectCommand({
104
+ Bucket: "foo-site",
105
+ Key: "index.html",
106
+ Body: "<h1>Hello, world!</h1>",
107
+ ContentType: "text/html; charset=utf-8",
108
+ }),
109
+ );
110
+ await simS3.putBucketWebsite(
111
+ new PutBucketWebsiteCommand({
112
+ Bucket: "foo-site",
113
+ WebsiteConfiguration: {
114
+ IndexDocument: {
115
+ Suffix: "index.html",
116
+ },
117
+ },
118
+ }),
119
+ );
120
+
121
+ // A website endpoint serves only what the Bucket policy makes readable, and a
122
+ // public policy needs the Block Public Access opt-out first.
123
+ await simS3.putPublicAccessBlock(
124
+ new PutPublicAccessBlockCommand({
125
+ Bucket: "foo-site",
126
+ PublicAccessBlockConfiguration: {
127
+ BlockPublicAcls: true,
128
+ IgnorePublicAcls: true,
129
+ },
130
+ }),
131
+ );
132
+ await simS3.putBucketPolicy(
133
+ new PutBucketPolicyCommand({
134
+ Bucket: "foo-site",
135
+ Policy: JSON.stringify({
136
+ Version: "2012-10-17",
137
+ Statement: {
138
+ Effect: "Allow",
139
+ Principal: "*",
140
+ Action: "s3:GetObject",
141
+ Resource: "arn:aws:s3:::foo-site/*",
142
+ },
143
+ }),
144
+ }),
145
+ );
146
+
147
+ const response = await simAwsHttp.fetch(simS3.getBucketWebsiteUrl("foo-site"));
148
+
149
+ console.log(response.status); // 200
150
+ console.log(await response.text()); // <h1>Hello, world!</h1>
151
+ ```
152
+
153
+ `fetch(input, init)` takes what the global `fetch` takes. `handleRequest(request)` takes a `Request`
154
+ that is already built, for a caller that is holding one.
155
+
156
+ With no port bound there is no URL to adapt. The website URL above is fetched as it is, where a
157
+ served environment needs `srv.localUrl(...)` to add the port it took. A hostname a simulated Route53
158
+ answers for is requested by its own name. A Distribution behind `www.example.com` is reached at
159
+ `https://www.example.com/`. An `https` URL needs no certificate set up for it, since there is no
160
+ connection to encrypt.
161
+
162
+ There is also no server to close, though `simAws.close()` still lets go of what the environment
163
+ itself is holding, such as a watched template file. See
164
+ [stopping and restarting](#stopping-and-restarting).
165
+
166
+ Live reload sits either side of this interface, outside it. A response from here never carries the
167
+ injected script, even in a process that served one elsewhere with live reload on.
168
+
169
+ Which to reach for:
170
+
171
+ - `SimAwsHttp` for tests, and for anything else in the same process. Test files that run in parallel
172
+ have no port to collide over. With no socket listening there is no teardown to get wrong, and no
173
+ connection between the request and the service answering it.
174
+ - `serveSimAws` for anything outside the process, such as a browser, `curl`, or an SDK client
175
+ pointed at a local endpoint.
176
+
177
+ Both go through the same authentication, routing and service code. A request answered one way is
178
+ answered the same way the other.
179
+
180
+ ## Pointing an AWS SDK or the CLI at the simulation
181
+
182
+ A served environment answers the general AWS service APIs on the same port it serves everything else. Give any AWS SDK, in any language, the server's own URL as its endpoint and it reaches the simulation.
183
+
184
+ ```typescript sim-serve-aws-api-endpoint
185
+ /**
186
+ * Reaching simulated DynamoDB with an ordinary SDK client over a port.
187
+ */
188
+
189
+ import {
190
+ CreateTableCommand,
191
+ DynamoDBClient,
192
+ PutItemCommand,
193
+ } from "@aws-sdk/client-dynamodb";
194
+ import {
195
+ CreateAccessKeyCommand,
196
+ CreateUserCommand,
197
+ PutUserPolicyCommand,
198
+ } from "@aws-sdk/client-iam";
199
+ import { SimAws } from "@kensio/yulin";
200
+ import { serveSimAws } from "@kensio/yulin/serve";
201
+
202
+ const simAws = new SimAws();
203
+
204
+ await simAws.dynamoDb().createTable(
205
+ new CreateTableCommand({
206
+ TableName: "widgets",
207
+ AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }],
208
+ KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
209
+ BillingMode: "PAY_PER_REQUEST",
210
+ }),
211
+ );
212
+
213
+ // A served request is authorized as whoever signed it, so the client needs
214
+ // credentials simulated IAM issued.
215
+ const simIam = simAws.iam();
216
+ await simIam.createUser(new CreateUserCommand({ UserName: "Widgets" }));
217
+ await simIam.putUserPolicy(
218
+ new PutUserPolicyCommand({
219
+ UserName: "Widgets",
220
+ PolicyName: "WriteWidgets",
221
+ PolicyDocument: JSON.stringify({
222
+ Version: "2012-10-17",
223
+ Statement: { Effect: "Allow", Action: "dynamodb:*", Resource: "*" },
224
+ }),
225
+ }),
226
+ );
227
+ const created = await simIam.createAccessKey(
228
+ new CreateAccessKeyCommand({ UserName: "Widgets" }),
229
+ );
230
+
231
+ const srv = await serveSimAws({ simAws, port: 8787 });
232
+
233
+ const client = new DynamoDBClient({
234
+ region: simAws.defaultRegionName,
235
+ endpoint: `http://localhost:${srv.port}`,
236
+ credentials: {
237
+ accessKeyId: created.AccessKey.AccessKeyId,
238
+ secretAccessKey: created.AccessKey.SecretAccessKey,
239
+ },
240
+ });
241
+
242
+ await client.send(
243
+ new PutItemCommand({
244
+ TableName: "widgets",
245
+ Item: { id: { S: "w1" } },
246
+ }),
247
+ );
248
+
249
+ await srv.close();
250
+ ```
251
+
252
+ One endpoint URL covers every service. A client sends `Host: localhost:<port>` whichever service it is talking to, which leaves no hostname to route on. Routing is on the service and Region named in the request's SigV4 credential scope, and a client cannot change either without invalidating its signature.
253
+
254
+ The same URL works for anything that speaks the AWS APIs, which includes the real `aws` CLI:
255
+
256
+ ```bash
257
+ export AWS_ENDPOINT_URL=http://localhost:8787
258
+ export AWS_ACCESS_KEY_ID=<key from simulated IAM>
259
+ export AWS_SECRET_ACCESS_KEY=<secret from simulated IAM>
260
+ aws dynamodb put-item --table-name widgets --item '{"id":{"S":"w1"}}'
261
+ ```
262
+
263
+ ### Who a served request is
264
+
265
+ Whoever signed it. The endpoint verifies the signature against simulated IAM and runs the operation as the principal behind the access key. An IAM policy applies exactly as it does in process, and an assumed-role session is authorized against the Role behind it.
266
+
267
+ A request carrying no signature is anonymous and reaches nothing. In process an omitted caller means "whoever owns this simulation", and over a port the same default would hand administrator rights to anyone who could reach it.
268
+
269
+ ### Which services answer
270
+
271
+ S3, STS, IAM, ELBv2, SNS, CloudFormation, Lambda, and the services that speak the AWS JSON protocol. Those are DynamoDB, DynamoDB Streams, SQS, Cognito Identity Provider, EventBridge, ECS, SSM, ACM, CloudWatch, CloudWatch Logs, KMS, Secrets Manager and Rekognition.
272
+
273
+ A request to any other service is refused with `501 Not Implemented` and a body saying why. Every service is reachable in process through `SimAws` and through [SDK interception](https://yulinsim.dev/sdk/), whether or not it answers here.
274
+
275
+ CloudWatch's windowed reads, `GetMetricStatistics` and `GetMetricData`, are the exception among the operations those services implement. The JSON protocol carries a timestamp as epoch seconds, and the endpoint passes that number through as it arrives. The simulation is handed a number where it expects a date. Both reads answer in process and through SDK interception.
276
+
277
+ ### Checking who the simulator thinks you are
278
+
279
+ `sts get-caller-identity` reports the principal behind the credentials that signed the request, which is the quickest way to confirm an endpoint and a set of credentials are wired up as expected:
280
+
281
+ ```bash
282
+ export AWS_ENDPOINT_URL=http://localhost:8787
283
+ aws sts get-caller-identity
284
+ {
285
+ "UserId": "AIDAM7J2TJYHV8BHEVIO",
286
+ "Account": "888888888888",
287
+ "Arn": "arn:aws:iam::888888888888:user/Widgets"
288
+ }
289
+ ```
290
+
291
+ An assumed-role session reports its own session ARN, as it does in real AWS, and its user id joins the Role's id to the session name. A caller with no identity is refused, since there is nothing to answer with.
292
+
293
+ ### Assuming a Role over the endpoint
294
+
295
+ `sts assume-role` answers with temporary credentials, and those credentials sign the requests that
296
+ follow:
297
+
298
+ ```bash
299
+ export AWS_ENDPOINT_URL=http://localhost:8787
300
+ aws sts assume-role --role-arn arn:aws:iam::888888888888:role/Reader --role-session-name probe
301
+ {
302
+ "Credentials": {
303
+ "AccessKeyId": "ASIAQ3JZQ6XKFPWLZ4TM",
304
+ "SecretAccessKey": "T4rBqYbLXKsJ0nZuV9dHc2Wm1PfAeR7gSjNyIvXo",
305
+ "SessionToken": "IQoJb3JpZ2luX2VjEHkaCXVzLWVhc3QtMSJHMEUCIQ",
306
+ "Expiration": "2026-08-18T21:00:00.000Z"
307
+ },
308
+ "AssumedRoleUser": {
309
+ "AssumedRoleId": "AROA5KQZH2XWNDLB7YTVR:probe",
310
+ "Arn": "arn:aws:sts::888888888888:assumed-role/Reader/probe"
311
+ }
312
+ }
313
+ ```
314
+
315
+ Hand the three credential values to an SDK client, or to `AWS_ACCESS_KEY_ID`,
316
+ `AWS_SECRET_ACCESS_KEY` and `AWS_SESSION_TOKEN`, and the whole assume-then-work sequence runs
317
+ against the endpoint URL. Simulated IAM authorizes each request against the Role behind the session,
318
+ and `get-caller-identity` reports the session ARN above.
319
+
320
+ `DurationSeconds` and `ExternalId` are read from the request as they are in process. The expiry is
321
+ stamped from the simulation's own clock. `simAws.clock().advanceBy({ hours: 2 })` takes a session
322
+ past it, and the credentials stop authenticating. An SDK refreshes an expired session by assuming
323
+ again.
324
+
325
+ A Role whose trust policy refuses the caller comes back as `AccessDenied`, under the name real STS
326
+ raises it.
327
+
328
+ `AssumeRole` and `GetCallerIdentity` are the two operations simulated STS implements, and both are
329
+ served. `AssumeRoleWithWebIdentity` and `GetSessionToken` are refused as `NotImplemented`.
330
+
331
+ ### IAM over the endpoint
332
+
333
+ A served request runs as whoever signed it, and the credentials to sign one used to come from the
334
+ process that built the simulation. `aws iam` closes that circle. A container or a shell script
335
+ creates its own User over the endpoint, gives it a policy, asks for an access key and signs
336
+ everything after that with what it was answered:
337
+
338
+ ```bash
339
+ export AWS_ENDPOINT_URL=http://localhost:8787
340
+ aws iam create-user --user-name widgets
341
+ aws iam put-user-policy --user-name widgets --policy-name everything \
342
+ --policy-document '{"Version":"2012-10-17","Statement":{"Effect":"Allow","Action":"*","Resource":"*"}}'
343
+ aws iam create-access-key --user-name widgets
344
+ ```
345
+
346
+ `create-access-key` answers with the secret, and nothing reports it again. The first request still
347
+ has to be signed by somebody. The [example above](#pointing-an-aws-sdk-or-the-cli-at-the-simulation)
348
+ builds one identity in process, and everything after it can be built over the port.
349
+
350
+ The fifteen operations simulated IAM implements are:
351
+
352
+ - **Users** — `CreateUser`, `CreateAccessKey`, `PutUserPolicy`
353
+ - **Roles** — `CreateRole`, `GetRole`, `ListRoles`, `DeleteRole`, `AttachRolePolicy`,
354
+ `DetachRolePolicy`, `PutRolePolicy`, `DeleteRolePolicy`
355
+ - **Managed policies** — `CreatePolicy`, `GetPolicy`, `ListPolicies`, `DeletePolicy`
356
+
357
+ Anything else is refused as `NotImplemented`, which an SDK raises under that name.
358
+
359
+ ### ELBv2 over the endpoint
360
+
361
+ `aws elbv2` builds an Application Load Balancer in the simulation over the same endpoint URL:
362
+
363
+ ```bash
364
+ export AWS_ENDPOINT_URL=http://localhost:8787
365
+ aws elbv2 create-load-balancer --name shop-alb --subnets subnet-1 subnet-2
366
+ aws elbv2 create-target-group --name checkout-tg --target-type lambda
367
+ aws elbv2 create-listener --load-balancer-arn <arn> --protocol HTTP --port 80 \
368
+ --default-actions Type=forward,TargetGroupArn=<arn>
369
+ ```
370
+
371
+ A load balancer built this way is the same load balancer an in-process build produces. Its DNS name
372
+ is served on the local port, and a request to it reaches the simulated Functions and ECS Services
373
+ registered behind it.
374
+
375
+ The twenty-two operations simulated ELBv2 implements are:
376
+
377
+ - **Load balancers** — `CreateLoadBalancer`, `DescribeLoadBalancers`, `DeleteLoadBalancer`
378
+ - **Target groups** — `CreateTargetGroup`, `DescribeTargetGroups`, `ModifyTargetGroup`,
379
+ `DeleteTargetGroup`
380
+ - **Targets** — `RegisterTargets`, `DeregisterTargets`, `DescribeTargetHealth`
381
+ - **Listeners** — `CreateListener`, `DescribeListeners`, `ModifyListener`, `DeleteListener`
382
+ - **Listener certificates** — `AddListenerCertificates`, `RemoveListenerCertificates`,
383
+ `DescribeListenerCertificates`
384
+ - **Rules** — `CreateRule`, `DescribeRules`, `ModifyRule`, `DeleteRule`, `SetRulePriorities`
385
+
386
+ A rule matches on `host-header` and `path-pattern`, in either the plain `Values` form or the
387
+ per-field configuration. A condition naming any other field is refused by name, as is an operation
388
+ outside the list above.
389
+
390
+ ### S3 over the endpoint
391
+
392
+ `aws s3` and an `S3Client` reach simulated S3 through the same endpoint URL:
393
+
394
+ ```bash
395
+ export AWS_ENDPOINT_URL=http://localhost:8787
396
+ aws s3api create-bucket --bucket widgets
397
+ aws s3api put-object --bucket widgets --key one.txt --body ./one.txt
398
+ aws s3 ls s3://widgets/
399
+ ```
400
+
401
+ An SDK client needs `forcePathStyle`, because a virtual-host request puts the Bucket in the hostname and this endpoint routes on the credential scope rather than the host:
402
+
403
+ ```typescript
404
+ const client = new S3Client({
405
+ region: "us-east-1",
406
+ endpoint: `http://localhost:${srv.port}`,
407
+ forcePathStyle: true,
408
+ credentials,
409
+ });
410
+ ```
411
+
412
+ The operations served are the ones simulated S3 implements: `CreateBucket`, `DeleteBucket`, `HeadBucket`, `ListBuckets`, `ListObjects`, `ListObjectsV2`, `GetObject`, `HeadObject`, `PutObject`, `DeleteObject`, `DeleteObjects`, the six multipart upload operations, and the Bucket policy, website, Block Public Access and event notification configurations. `aws s3 cp` works in both directions, and for a file above the CLI's 8MB multipart threshold. A `GetObject` carrying a `Range` is answered `206 Partial Content` with the bytes it asked for, and that is how the CLI takes a large file back out. Anything else is refused as `NotImplemented`, which an SDK raises under that name rather than leaving a client to guess.
413
+
414
+ Simulated S3 also answers its own Bucket hostnames, covered above. That path is unchanged, and it is what a website visitor uses.
415
+
416
+ A presigned URL reaches an Object either way. `getSignedUrl` signs for whatever endpoint its client was configured with, and a URL signed for this one states its credential scope in an `X-Amz-Credential` parameter. The endpoint reads the scope from there whenever a URL states one, since a presigned URL carries no `Authorization` header of its own. Set `forcePathStyle` on the presigning client for the same reason an ordinary request needs it.
417
+
418
+ ### SNS over the endpoint
419
+
420
+ `aws sns` and an `SNSClient` reach simulated SNS through the same endpoint URL:
421
+
422
+ ```bash
423
+ export AWS_ENDPOINT_URL=http://localhost:8787
424
+ aws sns create-topic --name orders
425
+ aws sns publish --topic-arn arn:aws:sns:us-east-1:888888888888:orders --message hello
426
+ ```
427
+
428
+ A message published here fans out to the simulated Queues and Functions subscribed to the topic, the
429
+ same way an in-process publish does. Delivery runs on the background scheduler (as it does on real
430
+ SNS, after the publish has been answered), so a test reading the Queue afterwards waits on
431
+ `simAws.backgroundTasksComplete()` first.
432
+
433
+ The operations served are the sixteen simulated SNS implements:
434
+
435
+ - **Topics** — `CreateTopic`, `DeleteTopic`, `ListTopics`, `GetTopicAttributes`, `SetTopicAttributes`
436
+ - **Subscriptions** — `Subscribe`, `Unsubscribe`, `ListSubscriptions`, `ListSubscriptionsByTopic`,
437
+ `GetSubscriptionAttributes`, `SetSubscriptionAttributes`
438
+ - **Messages** — `Publish`, `PublishBatch`
439
+ - **SMS** — `CheckIfPhoneNumberIsOptedOut`, `ListPhoneNumbersOptedOut`, `OptInPhoneNumber`
440
+
441
+ Anything else is refused as `NotImplemented`, which an SDK raises under that name.
442
+
443
+ ### CloudFormation over the endpoint
444
+
445
+ `aws cloudformation` deploys a template into the simulation over the same endpoint URL:
446
+
447
+ ```bash
448
+ export AWS_ENDPOINT_URL=http://localhost:8787
449
+ aws cloudformation create-stack --stack-name site --template-body file://template.json
450
+ aws cloudformation describe-stacks
451
+ ```
452
+
453
+ The four operations simulated CloudFormation implements are `CreateStack`, `UpdateStack`,
454
+ `DeleteStack` and `DescribeStacks`. A deployment starts in the background and the call is answered
455
+ before the Resources exist, as real CloudFormation answers it. `describe-stacks` reports the status
456
+ it reached, and `waitForStackDeployComplete` waits for it in process.
457
+
458
+ ### Lambda over the endpoint
459
+
460
+ `aws lambda` and a `LambdaClient` reach simulated Lambda through the same endpoint URL:
461
+
462
+ ```bash
463
+ export AWS_ENDPOINT_URL=http://localhost:8787
464
+ aws lambda invoke --function-name orders --payload '{"id":1}' /tmp/out.json
465
+ cat /tmp/out.json
466
+ ```
467
+
468
+ The function runs in the process serving it, and what its handler returned is written to the file.
469
+ A handler that throws answers `200` with `FunctionError` set to `Unhandled`, and the payload holds
470
+ the error it threw. `--invocation-type Event` answers `202` and runs the handler on the background
471
+ scheduler. A test that goes on to read what the function did waits on
472
+ `simAws.backgroundTasksComplete()` first.
473
+
474
+ A function created over the endpoint carries its code as a zip archive, which is the shape the
475
+ `Code.ZipFile` member travels in over HTTP:
476
+
477
+ ```bash
478
+ aws lambda create-function --function-name orders \
479
+ --role arn:aws:iam::888888888888:role/OrdersRole \
480
+ --handler index.handler --runtime nodejs22.x \
481
+ --zip-file fileb://orders.zip
482
+ ```
483
+
484
+ The operations served are nineteen of the ones simulated Lambda implements:
485
+
486
+ - **Functions** — `CreateFunction`, `GetFunction`, `UpdateFunctionCode`,
487
+ `UpdateFunctionConfiguration`, `ListFunctions`, `DeleteFunction`, `Invoke`
488
+ - **Function URLs** — `CreateFunctionUrlConfig`, `GetFunctionUrlConfig`,
489
+ `UpdateFunctionUrlConfig`, `DeleteFunctionUrlConfig`, `ListFunctionUrlConfigs`
490
+ - **Permissions** — `AddPermission`, `RemovePermission`, `GetPolicy`
491
+ - **Event source mappings** — `CreateEventSourceMapping`, `GetEventSourceMapping`,
492
+ `ListEventSourceMappings`, `DeleteEventSourceMapping`
493
+
494
+ The version and alias operations have no route here yet, and reach the simulation through `SimAws`
495
+ or SDK interception instead.
496
+
497
+ Anything else is refused as `NotImplemented`, which an SDK raises under that name. The refusal names
498
+ the path it arrived at. `aws lambda get-function-configuration` reports that
499
+ `GET /2015-03-31/functions/{name}/configuration` is unserved, and an unimplemented operation sharing
500
+ a method with one that is served gets the same answer.
501
+
502
+ Simulated Lambda also answers its own Function URL hostnames, covered above. That path is unchanged,
503
+ and it is what a browser and a webhook reach.
504
+
505
+ ## Stopping and restarting
506
+
507
+ `close()` stops serving and lets go of everything Yulin was holding, leaving the process free to
508
+ exit. That is the HTTP port, the DNS port and the connections the server is holding, along with the
509
+ simulated environment it was serving with them. The environment covers the template files a
510
+ deployment is [watching](https://yulinsim.dev/services/cloudformation/#watching-a-template-file) and the
511
+ directories a [mount](https://yulinsim.dev/services/s3/#reloading-the-browser-when-the-directory-changes) is
512
+ watching, in whichever Account and Region each of them lives in. One call covers all of it. A script
513
+ that hangs on exit is not a hunt for the handle you missed.
514
+
515
+ It returns a promise that settles once the last thing the server had to say has gone. A script that
516
+ means to exit under its own steam has something to wait for. Yulin installs no signal handlers,
517
+ since a library taking over process signals gets in the way of whatever else the process is doing.
518
+ Call `close()` from your own handler:
519
+
520
+ ```typescript sim-serve-shutdown
521
+ /**
522
+ * Closing a served environment when the process is asked to stop.
523
+ */
524
+
525
+ import { serveSimAws } from "@kensio/yulin/serve";
526
+
527
+ const srv = await serveSimAws({ port: 8787 });
528
+
529
+ async function stopServing(): Promise<void> {
530
+ // Waiting means anything the server still had to say has gone before the
531
+ // process does.
532
+ await srv.close();
533
+ }
534
+
535
+ process.on("SIGTERM", () => {
536
+ void stopServing();
537
+ });
538
+ ```
539
+
540
+ Closing twice is safe, and so is closing a server whose environment started nothing. What closes is
541
+ the handles that keep the process alive. Every simulated Bucket, Table and Stack is where it was,
542
+ and the environment goes on working. A script can close and carry on.
543
+
544
+ An unserved environment has the same call on it. `simAws.close()` lets go of its template file
545
+ watches and mounted directory watches, and a test with one of those closes it in one line:
546
+
547
+ ```typescript sim-serve-close-environment
548
+ /**
549
+ * Letting go of what an unserved environment is holding.
550
+ */
551
+
552
+ import { SimAws } from "@kensio/yulin";
553
+
554
+ const simAws = new SimAws();
555
+
556
+ await simAws.cloudFormation().deployTemplateFile({
557
+ templatePath: "cdk.out/TestStack.template.json",
558
+ watch: true,
559
+ });
560
+
561
+ // The Stack, and everything the template deployed, is still there afterwards.
562
+ await simAws.close();
563
+ ```
564
+
565
+ ### Asking for a signal handler
566
+
567
+ The handler above is yours to write, and that is the point. Your script decides what a signal means
568
+ and in what order things happen. A script that wants the usual behaviour can ask for it instead, and
569
+ gets the same close on `SIGINT` and `SIGTERM`:
570
+
571
+ ```typescript sim-serve-close-on-signal
572
+ /**
573
+ * Asking for the signal handler rather than writing one.
574
+ */
575
+
576
+ import { SimAws } from "@kensio/yulin";
577
+ import { serveSimAws } from "@kensio/yulin/serve";
578
+
579
+ const simAws = new SimAws();
580
+ const srv = await serveSimAws({ simAws, port: 8787, liveReload: true });
581
+
582
+ // Build the simulated environment the pages are served from here.
583
+
584
+ // Asking is the whole of it. The handler closes the server and the environment
585
+ // it serves, and the process then exits on its own.
586
+ const stopListening = srv.closeOnSignal();
587
+
588
+ // A script that stops wanting the handler before the process ends takes it off
589
+ // again:
590
+ stopListening();
591
+ ```
592
+
593
+ The handler goes on at that call and no sooner. A process that never asks keeps its signals to
594
+ itself. `closeOnSignal({ signals: ["SIGHUP"] })` names other signals. The handlers come off as the
595
+ first one arrives, and a second Ctrl-C from someone who has waited long enough lands on Node's own
596
+ default and ends the process. Closing never exits the process itself. It lets go of what Yulin was
597
+ holding, and a process with no work left then exits on its own. `SimAws` has the same method, for an
598
+ unserved environment.
599
+
600
+ A restart usually overlaps the process it replaces. `listen` waits a couple of seconds for a pinned
601
+ port that is still held, then throws `SimAwsLocalPortInUse` naming the port. By then something other
602
+ than the outgoing process owns it.
603
+
604
+ ## Live reload
605
+
606
+ A page served from a simulated Bucket website, CloudFront distribution, Function URL or HTTP API has
607
+ only Yulin in its response path. Yulin is the one thing that can tell the browser to reload. Turning
608
+ `liveReload` on serves a reload channel and puts a small script into the HTML pages it serves to
609
+ browsers:
610
+
611
+ ```typescript sim-serve-live-reload
612
+ /**
613
+ * Serving with live reload, so a browser reloads itself when the process
614
+ * restarts.
615
+ */
616
+
617
+ import { SimAws } from "@kensio/yulin";
618
+ import { serveSimAws } from "@kensio/yulin/serve";
619
+
620
+ const simAws = new SimAws();
621
+ const srv = await serveSimAws({ simAws, port: 8787, liveReload: true });
622
+
623
+ // Build the simulated environment the pages are served from here.
624
+
625
+ async function stopServing(): Promise<void> {
626
+ // Waiting means the browsers hear about the restart before the process goes.
627
+ await srv.close();
628
+ }
629
+
630
+ process.on("SIGTERM", () => {
631
+ void stopServing();
632
+ });
633
+ ```
634
+
635
+ It is off by default, and every response is then byte for byte what it would otherwise be.
636
+
637
+ ### Reloading on a restart
638
+
639
+ Local development means restarting the process, because a changed setup script or Lambda handler
640
+ needs a fresh module graph. The script survives that on its own, with no supervisor process and no
641
+ shared state between the outgoing and incoming process.
642
+
643
+ The channel is Server-Sent Events, and the browser reconnects by itself. Each process has a boot id
644
+ and sends it to every page that connects. A page that reconnects and finds a different boot id knows
645
+ it is showing output from a process that has gone, and reloads. A blip on a still running process
646
+ hands back the same boot id, and the page carries on.
647
+
648
+ `close()` sends a `reloading` event before the connections go. A page then reads the gap it is about
649
+ to see as a restart, and not a server that has died. The page keeps its own appearance and gets a
650
+ `data-sim-aws-live-reload="reloading"` attribute on its `<html>` element, ready to style:
651
+
652
+ ```css
653
+ html[data-sim-aws-live-reload="reloading"] {
654
+ opacity: 0.6;
655
+ }
656
+ ```
657
+
658
+ The event has to reach the browser before the connection goes. `close()` sees the reload streams out
659
+ before it destroys anything else the server was holding, and its promise settles once they have
660
+ gone. A browser that has stopped answering is waited on for half a second and then dropped. A page
661
+ nobody is looking at cannot hold up a restart. Both ports are released before any of that waiting,
662
+ and a replacement process can take them straight away either way.
663
+
664
+ None of this needs a supervisor process. A dev script started from an IDE debugger gets browser
665
+ reload with the debugger attached throughout.
666
+
667
+ ### Reloading without a restart
668
+
669
+ For a change that needs no restart, such as new content in a simulated Bucket, `reload()` reloads
670
+ every connected browser:
671
+
672
+ ```typescript sim-serve-reload
673
+ /**
674
+ * Reloading connected browsers after changing simulated content in place.
675
+ */
676
+
677
+ import { CreateBucketCommand, PutObjectCommand } from "@aws-sdk/client-s3";
678
+ import { SimAws } from "@kensio/yulin";
679
+ import { serveSimAws } from "@kensio/yulin/serve";
680
+
681
+ const simAws = new SimAws();
682
+ const srv = await serveSimAws({ simAws, liveReload: true });
683
+
684
+ await simAws.s3().createBucket(new CreateBucketCommand({ Bucket: "foo-site" }));
685
+ await simAws.s3().putObject(
686
+ new PutObjectCommand({
687
+ Bucket: "foo-site",
688
+ Key: "index.html",
689
+ Body: "<h1>Changed</h1>",
690
+ ContentType: "text/html; charset=utf-8",
691
+ }),
692
+ );
693
+
694
+ srv.reload();
695
+
696
+ await srv.close();
697
+ ```
698
+
699
+ `reload()` throws when live reload is off.
700
+
701
+ You can also hand the server to something that reloads for you, either a
702
+ [mounted directory](#reloading-when-a-build-changes-a-mounted-directory) or a
703
+ [watched template file](#answering-a-change-instead-of-restarting), both as `{ reload: srv }`. A
704
+ watched template file refuses a server it could never reload as it is handed over, ahead of the
705
+ first change.
706
+
707
+ ### Reloading when a build changes a mounted directory
708
+
709
+ A Bucket mounted on a local directory is already reading the files a site generator writes. A
710
+ rebuild needs nothing copied into it. Hand the mount the server and it watches the directory and
711
+ reloads for you once the writes stop:
712
+
713
+ ```typescript sim-serve-mount-reload
714
+ /**
715
+ * A built site the process reloads the browser for, rather than restarting for.
716
+ */
717
+
718
+ import path from "node:path";
719
+
720
+ import { CreateBucketCommand } from "@aws-sdk/client-s3";
721
+ import { SimAws } from "@kensio/yulin";
722
+ import { serveSimAws } from "@kensio/yulin/serve";
723
+
724
+ const simAws = new SimAws();
725
+ const srv = await serveSimAws({ simAws, port: 8787, liveReload: true });
726
+
727
+ await simAws.s3().createBucket(new CreateBucketCommand({ Bucket: "site" }));
728
+
729
+ simAws
730
+ .s3()
731
+ .mountBucketFilesystem("site", path.join(process.cwd(), "articles/public"), {
732
+ reload: srv,
733
+ });
734
+ ```
735
+
736
+ One build is one reload, however many files it wrote. See
737
+ [filesystem-backed Bucket storage](https://yulinsim.dev/services/s3/#reloading-the-browser-when-the-directory-changes)
738
+ for the `settleMs` override and for stopping the watch.
739
+
740
+ ### What gets the script
741
+
742
+ Yulin's own account of a request goes in headers and never in the body. A response keeps the shape
743
+ the real service returns. Live reload breaks that rule on purpose, and only for a page a browser is
744
+ about to render. A response gets the script only when all of this holds:
745
+
746
+ - the response media type is `text/html` exactly
747
+ - the request `accept` header asks for `text/html`
748
+ - the request carries no SigV4 signature, in either an `authorization` header or the query string of
749
+ a presigned URL, and no `x-amz-*` header
750
+ - the response has no `content-encoding`
751
+ - the response status is other than 206, and the request is not a `HEAD`
752
+
753
+ So an SDK `GetObject` for an HTML Object gets the stored bytes, byte for byte, while a browser
754
+ looking at the same Object through a website endpoint gets the script.
755
+
756
+ An injected response carries `x-sim-aws-live-reload: injected`. Its `content-length` is recomputed,
757
+ and `etag` and `last-modified` are dropped, since the bytes are no longer the ones those headers
758
+ describe. Its `cache-control` is set to `no-store`, replacing whatever the service said, because a
759
+ page held in the browser cache is a page live reload cannot reach. The script goes in before
760
+ `</body>`, or before `</html>` when there is no body element, or on the end when the HTML has no
761
+ such element.
762
+
763
+ ### The reserved path
764
+
765
+ The reload channel is served at `/__sim-aws/live-reload` on every hostname the server answers on. A
766
+ page can ask for it relative to wherever it was served from. While live reload is on, no simulated
767
+ service can serve anything at that path.
768
+
769
+ ## Messages on the console
770
+
771
+ A simulated Cognito user pool records the verification message it would have sent, simulated SNS
772
+ records the text message it would have texted, and simulated SES records the email it accepted.
773
+ Reading any of them back takes test code, or the pool's `/<userPoolId>/messages` listing. Either is
774
+ a detour when the sign-up form is open in a browser and the confirmation code is the one thing you
775
+ want. A served environment prints them as they are recorded:
776
+
777
+ ```
778
+ sim Cognito eu-west-2_aBcDeFgHi: email to alice@example.com (SignUp)
779
+ Subject: Your verification code
780
+ Your confirmation code is 483920
781
+ sim SNS: SMS to +15550100
782
+ Your one-time code is 118221
783
+ sim SES: hello@example.com to alice@example.com
784
+ Subject: Welcome to Example
785
+ Text body:
786
+ Glad to have you here.
787
+ ```
788
+
789
+ Nothing has to ask for that. Narrow it with `messageLogging`, which holds one property per kind of
790
+ message:
791
+
792
+ ```typescript sim-serve-message-logging
793
+ /**
794
+ * Serving with the text messages left off, and the pool messages still
795
+ * printed.
796
+ */
797
+
798
+ import { SimAws } from "@kensio/yulin";
799
+ import { serveSimAws } from "@kensio/yulin/serve";
800
+
801
+ const simAws = new SimAws();
802
+ const srv = await serveSimAws({
803
+ simAws,
804
+ port: 8787,
805
+ messageLogging: { sns: false },
806
+ });
807
+
808
+ // Serve the pages that sign a user up here.
809
+
810
+ await srv.close();
811
+ ```
812
+
813
+ `messageLogging: false` prints none of them.
814
+
815
+ An SMS the opt-out list stopped is printed with the suppression named. The publish succeeded, and a
816
+ handset that will never see the code is worth being told about rather than waited on:
817
+
818
+ ```
819
+ sim SNS: SMS to +15550100 (suppressed, number opted out)
820
+ Your one-time code is 118221
821
+ ```
822
+
823
+ ### What an email prints as
824
+
825
+ An email is summarised. Printing an HTML part in full would run to kilobytes of markup and push the
826
+ sender, the recipients and the subject off the screen, and those are what say which send this was.
827
+ The first line carries the sender and the three recipient lists. The subject follows indented under
828
+ it, then the template the message was rendered from (where a stored template was) and its data, then
829
+ the text part. An HTML part is measured and left out:
830
+
831
+ ```
832
+ sim SES: hello@example.com to alice@example.com, bcc audit@example.com
833
+ Subject: Reset your password
834
+ Template: password-reset {"code":"483920"}
835
+ Text body:
836
+ Follow this link to reset your password.
837
+ https://app.example.com/reset?token=abc123
838
+ HTML body: 4.1 kB, not printed
839
+ ```
840
+
841
+ A text part is printed up to 2000 characters. What runs past that is counted and left out:
842
+
843
+ ```
844
+ Text body:
845
+ Here is your monthly statement in full.
846
+ ... 4162 more characters, not printed
847
+ ```
848
+
849
+ A user pool sending its email through simulated SES prints twice, once for the SES send and once for
850
+ the message the pool kept. Both services recorded it, and each block says what that service holds.
851
+ See [Sending a pool's email through SES](https://yulinsim.dev/services/cognito/#sending-a-pools-email-through-ses).
852
+
853
+ `emailTextLimit` moves that limit. It sits beside the per-kind switches on the same option:
854
+
855
+ ```typescript sim-serve-email-text-limit
856
+ /**
857
+ * Serving with a shorter limit on the email text that reaches the console.
858
+ */
859
+
860
+ import { SimAws } from "@kensio/yulin";
861
+ import { serveSimAws } from "@kensio/yulin/serve";
862
+
863
+ const simAws = new SimAws();
864
+ const srv = await serveSimAws({
865
+ simAws,
866
+ port: 8787,
867
+ messageLogging: { emailTextLimit: 500 },
868
+ });
869
+
870
+ // Serve the pages that send the email here.
871
+
872
+ await srv.close();
873
+ ```
874
+
875
+ Only what happens while the server is up reaches the console. A message recorded before it started
876
+ listening, or after `close()`, is on the service's own record and nowhere else. `sentMessages()`,
877
+ `sentSmsMessages()` and `sentEmails()` are still where the whole history is.
878
+
879
+ ## Restarting on a file change
880
+
881
+ Live reload gets a page back on its feet after the process restarts. `yulin watch` is what restarts
882
+ it. Run the dev script through it and a save is the whole loop:
883
+
884
+ ```bash
885
+ yulin watch -- tsx dev.ts
886
+ ```
887
+
888
+ Everything after `--` is the command, run as written and restarted when something changes. The CLI
889
+ never imports it, never looks for an exported setup function, and takes no interest in whether the
890
+ simulation was built from SDK commands, a CloudFormation template, or several `SimAws` instances at
891
+ once. The only change to a dev script is turning `liveReload` on.
892
+
893
+ Restarting is the deliberate choice, over swapping code in place. A Lambda handler is a function
894
+ reference out of your own module graph, and a new process re-imports it with no module cache to
895
+ defeat. It also keeps simulated state the same as what a fresh test run sees, since seeding is part
896
+ of the setup script and runs again.
897
+
898
+ ### What is watched
899
+
900
+ The working directory, minus the paths nobody edits by hand. Those are `node_modules`, `.git`,
901
+ `dist`, `coverage`, CDK asset directories, and the working files an editor writes around a save.
902
+
903
+ On top of that, Yulin names paths it is holding that the module graph never mentions. A directory
904
+ given to `mountBucketFilesystem` and a template given to `deployTemplateFile` are reported to the
905
+ supervisor as they are registered, and watched from then on, without appearing in any list. Editing
906
+ a file in a mounted directory or re-synthing a stack restarts the process.
907
+
908
+ A path the process is watching itself is the exception, and is
909
+ [left to the process reading it](#answering-a-change-instead-of-restarting). That covers a template
910
+ deployed with the `watch` option, and a directory mounted with somewhere to reload. So is any other
911
+ path the process [says it is holding](#holding-a-path-yourself).
912
+
913
+ ### One restart for a burst of writes
914
+
915
+ A burst of writes is one restart. Saving one file is several filesystem events, and changes are held
916
+ until they stop arriving before anything is restarted. The wait is 250ms by default. A build writing
917
+ hundreds of files gets one restart, however many files it wrote.
918
+
919
+ The number is set by what a build needs, and a save pays for it. macOS hands a recursive watch its
920
+ events in waves. A build writing several thousand files was measured arriving as tens of waves up to
921
+ 49ms apart, and a window anywhere near that turns one build into several restarts. A build that
922
+ pauses between its own phases, as a tool that resolves before it writes does, pauses for longer than
923
+ that again. 250ms clears the waves several times over and covers the shorter of those pauses, at the
924
+ cost of 250ms before a plain save is acted on.
925
+
926
+ A project whose build is unusual can say so on the command line:
927
+
928
+ ```bash
929
+ yulin watch --settle=600 -- tsx dev.ts
930
+ ```
931
+
932
+ Writes that keep arriving push the wait back. A build that never goes quiet would otherwise hold the
933
+ restart off for as long as it ran, so a burst is acted on after five seconds however much is still
934
+ arriving, and the writes after that are a burst of their own. That is a backstop for a build that
935
+ writes continuously for minutes. An ordinary build never reaches it.
936
+
937
+ ### Holding a path yourself
938
+
939
+ A process that is already watching a path and answering changes to it in place has nothing to gain
940
+ from a restart, and everything its simulation holds to lose. `simWatch.reportHeldPath(...)` says so,
941
+ and the supervisor leaves that path alone from then on:
942
+
943
+ ```typescript sim-serve-hold-path
944
+ /**
945
+ * A mounted directory this process watches itself, reloading the browser
946
+ * rather than being restarted for it.
947
+ */
948
+
949
+ import { watch } from "node:fs";
950
+ import path from "node:path";
951
+
952
+ import { CreateBucketCommand } from "@aws-sdk/client-s3";
953
+ import { SimAws } from "@kensio/yulin";
954
+ import { serveSimAws } from "@kensio/yulin/serve";
955
+ import { simWatch } from "@kensio/yulin/watch";
956
+
957
+ const built = path.join(process.cwd(), "public");
958
+
959
+ const simAws = new SimAws();
960
+ await simAws.s3().createBucket(new CreateBucketCommand({ Bucket: "site" }));
961
+ simAws.s3().mountBucketFilesystem("site", built);
962
+
963
+ const srv = await serveSimAws({ simAws, port: 8787, liveReload: true });
964
+
965
+ simWatch.reportHeldPath(built);
966
+
967
+ watch(built, { recursive: true }, () => {
968
+ srv.reload();
969
+ });
970
+ ```
971
+
972
+ A static site build writing into that directory then reloads the page, where the mount alone would
973
+ have restarted the process and taken every simulated Bucket, Table and Stack with it. Holding a path
974
+ beats having reported it. Being told a path is answered in the process is more specific than being
975
+ told it is worth watching, so holding wins even though `mountBucketFilesystem` reported the same
976
+ directory first.
977
+
978
+ A held path stays held only for the run that reported it. A run that exits releases the path, and
979
+ the supervisor watches it again until its replacement says otherwise.
980
+
981
+ `simWatch.onStopping(...)` is the other half, for a restart this process has no say in. It runs just
982
+ before the supervisor kills the process. Live reload uses that moment to tell browsers a reload is
983
+ coming, so a page comes back on its own. A hand-written reload channel wants the same warning.
984
+
985
+ Both are best effort and need a supervisor. In a process `yulin watch` never started, such as a test
986
+ run or a script launched from an IDE debugger, both are no-ops.
987
+
988
+ This is the general case, for a path nothing else knows about. A mounted directory is the one Yulin
989
+ does know about. `mountBucketFilesystem` takes a reload target, holds the path itself and settles
990
+ the writes, so the example above is written for you. See
991
+ [reloading when a build changes a mounted directory](#reloading-when-a-build-changes-a-mounted-directory).
992
+
993
+ ### Answering a change instead of restarting
994
+
995
+ A deployment that watches its own template file is left alone by the supervisor:
996
+
997
+ ```typescript sim-serve-watch-template
998
+ /**
999
+ * A template the process updates its stack from, rather than restarting for.
1000
+ */
1001
+
1002
+ import { SimAws } from "@kensio/yulin";
1003
+ import { serveSimAws } from "@kensio/yulin/serve";
1004
+
1005
+ const simAws = new SimAws();
1006
+ const srv = await serveSimAws({ simAws, port: 8787, liveReload: true });
1007
+
1008
+ await simAws.cloudFormation().deployTemplateFile({
1009
+ templatePath: "cdk.out/TestStack.template.json",
1010
+ watch: {
1011
+ reload: srv,
1012
+ },
1013
+ });
1014
+ ```
1015
+
1016
+ Re-synthing the stack then updates it in place and reloads the page. The reload waits for the update
1017
+ and not the write. The page comes back on the resources the new template asked for, and an update
1018
+ that failed reloads nothing. Whatever the change left alone keeps what it holds in simulated S3,
1019
+ DynamoDB and SQS, where a restart would have taken all of it. The process names the template as one
1020
+ it is answering itself, and the supervisor takes it off its own list. See
1021
+ [watching a template file](https://yulinsim.dev/services/cloudformation/#watching-a-template-file) for what
1022
+ an update does to the resources.
1023
+
1024
+ A directory mounted with somewhere to reload is left alone the same way. The Bucket is reading the
1025
+ files either way, and a rebuild has nothing to redo. The browser is reloaded, and everything else
1026
+ the process is holding stays where it is. A restart would have taken the whole simulated environment
1027
+ for the sake of a page that changed. See
1028
+ [reloading when a build changes a mounted directory](#reloading-when-a-build-changes-a-mounted-directory).
1029
+
1030
+ A template synthesized against a real account sometimes needs adapting before Yulin will take it.
1031
+ `transform` is given the parsed template and answers with the one to deploy, on the deployment and
1032
+ again on every change. The file the supervisor leaves alone is still the one in `cdk.out`:
1033
+
1034
+ ```typescript sim-serve-transform-template
1035
+ /**
1036
+ * Adapting a watched template, so the file being watched is the real one.
1037
+ */
1038
+
1039
+ import { SimAws } from "@kensio/yulin";
1040
+ import { serveSimAws } from "@kensio/yulin/serve";
1041
+
1042
+ const simAws = new SimAws();
1043
+ const srv = await serveSimAws({ simAws, port: 8787, liveReload: true });
1044
+
1045
+ await simAws.cloudFormation().deployTemplateFile({
1046
+ templatePath: "cdk.out/TestStack.template.json",
1047
+ transform: (template) => ({
1048
+ ...template,
1049
+ Resources: Object.fromEntries(
1050
+ Object.entries(template.Resources).filter(
1051
+ ([logicalId]) => logicalId !== "SiteAliasRecord",
1052
+ ),
1053
+ ),
1054
+ }),
1055
+ watch: {
1056
+ onUpdated: () => {
1057
+ srv.reload();
1058
+ },
1059
+ },
1060
+ });
1061
+ ```
1062
+
1063
+ A transform that throws is reported the way a failed update is. The process and the resources it is
1064
+ serving are left where they were. See
1065
+ [adapting a synthesized template](https://yulinsim.dev/services/cloudformation/#adapting-a-synthesized-template-on-the-way-in).
1066
+
1067
+ Both work with no supervisor at all. A dev script started from an IDE debugger picks up a re-synth
1068
+ or a rebuild with the debugger attached throughout.
1069
+
1070
+ ### When a run goes wrong
1071
+
1072
+ A setup script that throws leaves the watcher up. The error is on the terminal and the next save is
1073
+ the retry, with no watch to start over.
1074
+
1075
+ Setup that writes into a watched path restarts the process, which writes again, which restarts it.
1076
+ `yulin watch` refuses to run that loop. After a few restarts caused by the same file changing
1077
+ straight after startup, it stops and names the file. Write generated files outside the working
1078
+ directory, or into a directory the watch passes over.
1079
+
1080
+ ### Debugging
1081
+
1082
+ A process started by `yulin watch` can be debugged. Attach the debugger to that process, and not to
1083
+ the supervisor, which has nothing worth stepping through. Pass an inspector flag to the watch and it
1084
+ reaches each run through `NODE_OPTIONS`, whether the command is `node` or something that spawns it:
1085
+
1086
+ ```bash
1087
+ yulin watch --inspect=9230 -- tsx dev.ts
1088
+ ```
1089
+
1090
+ Each run binds the same inspector port, because the process it replaces has fully exited by the time
1091
+ the replacement starts. Attaching to that port with reconnect turned on in your IDE keeps a debugger
1092
+ across restarts. The exact run configuration differs between IDEs.
1093
+
1094
+ Live reload works without the supervisor. A dev script launched straight from an IDE debugger still
1095
+ gets browser reload and still picks up a re-synthed template. A handler edit is a manual restart, as
1096
+ it is without watch mode.
1097
+
1098
+ ## Limitations
1099
+
1100
+ - An injected page is not byte for byte what the real service would return, and its `cache-control`
1101
+ is the simulator's rather than the service's. That is the point of the feature, and the reason it
1102
+ is off by default and says so on startup.
1103
+ - The `Location` header is the only place a simulated hostname is rewritten for a browser. A
1104
+ hostname in a page body, in a JSON response or in a cookie `Domain` is left as the service wrote
1105
+ it.
1106
+ - `/__sim-aws/live-reload` is shadowed on every served hostname while live reload is on.
1107
+ - Console message logging covers simulated Cognito messages and simulated SNS text messages. An
1108
+ email accepted by simulated SES is recorded and left unprinted, because a body can run to
1109
+ kilobytes of HTML.
1110
+ - Injection decodes the HTML as UTF-8. A page stored in another encoding would be corrupted, so
1111
+ serve HTML as UTF-8.
1112
+ - An open reload connection uses one of the browser's six connections per origin per tab.
1113
+ - There is no overlay for a reload that failed. The terminal has the error.
1114
+ - `yulin watch` does not re-synth CDK. `cdk watch` is a shortcut for `deploy --watch` against real
1115
+ AWS and there is no synth-only watch, so run your own synth and let the watch pick up its output.
1116
+ - A template updated in place still replaces a changed resource, so the objects in a bucket the
1117
+ change touches go with it. Only the resources the template left alone keep what they hold.
1118
+ - Lambda and CloudFront Function code is not swapped without a restart. A fresh process picks up an
1119
+ edited handler correctly, and an in-process swap would have to invalidate an ESM import subgraph
1120
+ that the language keeps private.
1121
+ - Simulated state is not carried across a restart. Seeding belongs in the setup script, so it runs
1122
+ again and local state stays the same as what tests and CI see.
1123
+ - The IDE run configurations for attaching a debugger to a watched process are not documented yet.
1124
+ - The served AWS service API covers S3, STS and the AWS JSON protocol services. A service speaking REST-JSON, or Query other than STS, is refused with `501 Not Implemented`.
1125
+ - Simulated STS implements `AssumeRole` and `GetCallerIdentity`, and serves both. `AssumeRoleWithWebIdentity` and `GetSessionToken` are refused as `NotImplemented`.
1126
+ - A served AWS API request is routed by its SigV4 credential scope. An unsigned one reaches nothing, whatever endpoint URL it used.