@kensio/yulin 1.21.6 → 1.21.8

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 (74) hide show
  1. package/README.md +22 -2
  2. package/dist/service/eventbridge/cfn/bus/sim-cfn-event-bus-creator.js +1 -0
  3. package/dist/service/eventbridge/cfn/bus/sim-cfn-event-bus-properties.d.ts +4 -0
  4. package/dist/service/eventbridge/cfn/bus/sim-cfn-event-bus-properties.js +26 -1
  5. package/dist/service/eventbridge/cfn/rule/sim-cfn-event-rule-creator.js +3 -1
  6. package/dist/service/eventbridge/cfn/rule/sim-cfn-event-rule-properties.d.ts +0 -7
  7. package/dist/service/eventbridge/cfn/rule/sim-cfn-event-rule-properties.js +0 -20
  8. package/dist/service/eventbridge/cfn/rule/sim-cfn-event-rule-unsimulated-properties.d.ts +13 -0
  9. package/dist/service/eventbridge/cfn/rule/sim-cfn-event-rule-unsimulated-properties.js +59 -0
  10. package/dist/service/eventbridge/cfn/sim-cfn-event-bridge-resource-error.d.ts +1 -1
  11. package/dist/service/eventbridge/cfn/sim-cfn-event-bridge-resource-error.js +1 -1
  12. package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-properties.js +1 -1
  13. package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-property-rules.d.ts +3 -2
  14. package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-property-rules.js +25 -3
  15. package/dist/service/ses/command/authorize/sim-ses-authorizer.d.ts +9 -0
  16. package/dist/service/ses/command/authorize/sim-ses-authorizer.js +18 -2
  17. package/dist/service/ses/command/send/sim-ses-send-email.js +2 -1
  18. package/dist/service/sns/cfn/topic/sim-cfn-sns-topic-creator.js +1 -0
  19. package/dist/service/sns/cfn/topic/sim-cfn-sns-topic-properties.d.ts +9 -4
  20. package/dist/service/sns/cfn/topic/sim-cfn-sns-topic-properties.js +18 -6
  21. package/dist/service/sns/cfn/topic/sim-cfn-sns-topic-property-names.d.ts +10 -0
  22. package/dist/service/sns/cfn/topic/sim-cfn-sns-topic-property-names.js +16 -1
  23. package/dist/service/ssm/cfn/parameter/sim-cfn-ssm-parameter-creator.js +1 -1
  24. package/dist/service/ssm/cfn/parameter/sim-cfn-ssm-parameter-properties.d.ts +11 -6
  25. package/dist/service/ssm/cfn/parameter/sim-cfn-ssm-parameter-properties.js +18 -9
  26. package/docs/README.md +40 -4
  27. package/docs/ai-skill/README.md +57 -54
  28. package/docs/cli/README.md +84 -94
  29. package/docs/factories/README.md +42 -54
  30. package/docs/lint/README.md +41 -67
  31. package/docs/non-aws-dependencies/README.md +72 -168
  32. package/docs/sdk/README.md +109 -95
  33. package/docs/serve/README.md +192 -898
  34. package/docs/services/acm/README.md +24 -40
  35. package/docs/services/apigateway/README.md +52 -71
  36. package/docs/services/apigatewayv2/README.md +55 -74
  37. package/docs/services/athena/README.md +17 -26
  38. package/docs/services/backup/README.md +29 -39
  39. package/docs/services/bedrock/README.md +38 -52
  40. package/docs/services/cloudformation/README.md +43 -55
  41. package/docs/services/cloudfront/README.md +69 -95
  42. package/docs/services/cloudwatch/README.md +40 -54
  43. package/docs/services/cognito/README.md +30 -45
  44. package/docs/services/dynamodb/README.md +34 -51
  45. package/docs/services/ecr/README.md +36 -77
  46. package/docs/services/ecs/README.md +26 -46
  47. package/docs/services/elbv2/README.md +19 -29
  48. package/docs/services/eventbridge/README.md +24 -20
  49. package/docs/services/firehose/README.md +24 -32
  50. package/docs/services/glue/README.md +41 -78
  51. package/docs/services/iam/README.md +13 -15
  52. package/docs/services/kinesis/README.md +53 -93
  53. package/docs/services/kms/README.md +22 -32
  54. package/docs/services/lambda/README.md +60 -80
  55. package/docs/services/logs/README.md +41 -50
  56. package/docs/services/organizations/README.md +50 -85
  57. package/docs/services/personalize/README.md +28 -44
  58. package/docs/services/rekognition/README.md +26 -38
  59. package/docs/services/route53/README.md +17 -17
  60. package/docs/services/s3/README.md +47 -51
  61. package/docs/services/scheduler/README.md +41 -52
  62. package/docs/services/secretsmanager/README.md +27 -42
  63. package/docs/services/ses/README.md +24 -34
  64. package/docs/services/sns/README.md +31 -33
  65. package/docs/services/sqs/README.md +14 -14
  66. package/docs/services/ssm/README.md +19 -21
  67. package/docs/services/stepfunctions/README.md +18 -20
  68. package/docs/services/sts/README.md +32 -45
  69. package/docs/services/wafv2/README.md +12 -17
  70. package/docs/terraform/README.md +108 -126
  71. package/docs/testing/README.md +228 -0
  72. package/docs/time/README.md +87 -117
  73. package/llms.txt +2 -1
  74. package/package.json +1 -1
@@ -1,87 +1,55 @@
1
- # Localhost server
1
+ # Serve Yulin on localhost
2
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.
3
+ Yulin can handle HTTP and DNS requests either in process or through local network ports.
6
4
 
7
- ## Serving a port
5
+ ## Start a local server
8
6
 
9
- `serveSimAws` starts the server and returns once it is listening:
7
+ Use `serveSimAws` when a browser, command-line program, or application in another process needs to
8
+ reach the simulation:
10
9
 
11
10
  ```typescript sim-serve-localhost
12
11
  /**
13
- * Serving a simulated environment on a port of your choosing.
12
+ * Serving a simulated AWS environment on localhost.
14
13
  */
15
14
 
16
15
  import { SimAws } from "@kensio/yulin";
17
16
  import { serveSimAws } from "@kensio/yulin/serve";
18
17
 
19
18
  const simAws = new SimAws();
20
- const srv = await serveSimAws({ simAws, port: 8787 });
19
+ const server = await serveSimAws({ simAws, port: 8787 });
21
20
 
22
- console.log(srv.port); // "8787"
21
+ console.log(server.port); // "8787"
22
+ console.log(server.hostname); // "sim-aws.localhost"
23
23
 
24
- await srv.close();
24
+ await server.close();
25
25
  ```
26
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.
27
+ Omit `port` to let the operating system choose a free port. Read the selected value from
28
+ `server.port`.
29
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:
30
+ The server keeps simulated service hostnames in the URL. `server.localUrl(...)` converts a URL
31
+ returned by Yulin into an address served by this server:
32
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:
33
+ ```typescript
34
+ const localUrl = server.localUrl(
35
+ "https://my-site.s3-website.eu-west-2.sim-aws.localhost/index.html",
36
+ );
63
37
 
64
- ```http
65
- 303 See Other
66
- Location: http://auth.example.com.sim-aws.localhost:8787/oauth2/authorize?client_id=1a2b3c
38
+ console.log(localUrl.toString());
39
+ // http://my-site.s3-website.eu-west-2.sim-aws.localhost:8787/index.html
67
40
  ```
68
41
 
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.
42
+ The server also rewrites an absolute `Location` response header when it points to a hostname in the
43
+ simulation. Relative redirects and redirects to unknown hosts are left unchanged.
73
44
 
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.
45
+ ## Send HTTP requests in process
76
46
 
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:
47
+ Use `SimAwsHttp` for tests and other callers in the same process. It accepts the same inputs as
48
+ `fetch`, but it opens no socket:
81
49
 
82
50
  ```typescript sim-serve-in-process-request
83
51
  /**
84
- * Requesting a simulated S3 website with no server listening.
52
+ * Requesting a simulated S3 website without starting a server.
85
53
  */
86
54
 
87
55
  import {
@@ -95,95 +63,69 @@ import { SimAws } from "@kensio/yulin";
95
63
  import { SimAwsHttp } from "@kensio/yulin/serve";
96
64
 
97
65
  const simAws = new SimAws();
98
- const simAwsHttp = new SimAwsHttp({ simAws });
99
- const simS3 = simAws.region("eu-west-2").s3();
66
+ const http = new SimAwsHttp({ simAws });
67
+ const s3 = simAws.region("eu-west-2").s3();
100
68
 
101
- await simS3.createBucket(new CreateBucketCommand({ Bucket: "foo-site" }));
102
- await simS3.putObject(
69
+ await s3.createBucket(new CreateBucketCommand({ Bucket: "site" }));
70
+ await s3.putObject(
103
71
  new PutObjectCommand({
104
- Bucket: "foo-site",
72
+ Bucket: "site",
105
73
  Key: "index.html",
106
- Body: "<h1>Hello, world!</h1>",
74
+ Body: "<h1>Hello</h1>",
107
75
  ContentType: "text/html; charset=utf-8",
108
76
  }),
109
77
  );
110
- await simS3.putBucketWebsite(
78
+ await s3.putBucketWebsite(
111
79
  new PutBucketWebsiteCommand({
112
- Bucket: "foo-site",
113
- WebsiteConfiguration: {
114
- IndexDocument: {
115
- Suffix: "index.html",
116
- },
117
- },
80
+ Bucket: "site",
81
+ WebsiteConfiguration: { IndexDocument: { Suffix: "index.html" } },
118
82
  }),
119
83
  );
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(
84
+ await s3.putPublicAccessBlock(
124
85
  new PutPublicAccessBlockCommand({
125
- Bucket: "foo-site",
86
+ Bucket: "site",
126
87
  PublicAccessBlockConfiguration: {
127
88
  BlockPublicAcls: true,
128
89
  IgnorePublicAcls: true,
129
90
  },
130
91
  }),
131
92
  );
132
- await simS3.putBucketPolicy(
93
+ await s3.putBucketPolicy(
133
94
  new PutBucketPolicyCommand({
134
- Bucket: "foo-site",
95
+ Bucket: "site",
135
96
  Policy: JSON.stringify({
136
97
  Version: "2012-10-17",
137
98
  Statement: {
138
99
  Effect: "Allow",
139
100
  Principal: "*",
140
101
  Action: "s3:GetObject",
141
- Resource: "arn:aws:s3:::foo-site/*",
102
+ Resource: "arn:aws:s3:::site/*",
142
103
  },
143
104
  }),
144
105
  }),
145
106
  );
146
107
 
147
- const response = await simAwsHttp.fetch(simS3.getBucketWebsiteUrl("foo-site"));
108
+ const response = await http.fetch(s3.getBucketWebsiteUrl("site"));
148
109
 
149
110
  console.log(response.status); // 200
150
- console.log(await response.text()); // <h1>Hello, world!</h1>
111
+ console.log(await response.text()); // <h1>Hello</h1>
151
112
  ```
152
113
 
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.
114
+ `SimAwsHttp.fetch(input, init)` builds a Fetch API `Request` for you.
115
+ `SimAwsHttp.handleRequest(request)` accepts a request that is already built. Both methods use the
116
+ same routing, authentication, and service controllers as the local server.
168
117
 
169
- Which to reach for:
118
+ `SimAwsHttp` uses simulated hostnames directly. HTTPS URLs also work because no network connection
119
+ or TLS handshake takes place.
170
120
 
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.
121
+ ## Connect an AWS SDK client
176
122
 
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.
123
+ The local server exposes supported AWS service APIs through one endpoint URL. The client still
124
+ needs a Region and credentials issued by simulated IAM:
183
125
 
184
126
  ```typescript sim-serve-aws-api-endpoint
185
127
  /**
186
- * Reaching simulated DynamoDB with an ordinary SDK client over a port.
128
+ * Calling simulated DynamoDB through a local HTTP endpoint.
187
129
  */
188
130
 
189
131
  import {
@@ -210,29 +152,30 @@ await simAws.dynamoDb().createTable(
210
152
  }),
211
153
  );
212
154
 
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(
155
+ const iam = simAws.iam();
156
+ await iam.createUser(new CreateUserCommand({ UserName: "Operator" }));
157
+ await iam.putUserPolicy(
218
158
  new PutUserPolicyCommand({
219
- UserName: "Widgets",
159
+ UserName: "Operator",
220
160
  PolicyName: "WriteWidgets",
221
161
  PolicyDocument: JSON.stringify({
222
162
  Version: "2012-10-17",
223
- Statement: { Effect: "Allow", Action: "dynamodb:*", Resource: "*" },
163
+ Statement: {
164
+ Effect: "Allow",
165
+ Action: "dynamodb:PutItem",
166
+ Resource: "*",
167
+ },
224
168
  }),
225
169
  }),
226
170
  );
227
- const created = await simIam.createAccessKey(
228
- new CreateAccessKeyCommand({ UserName: "Widgets" }),
171
+ const created = await iam.createAccessKey(
172
+ new CreateAccessKeyCommand({ UserName: "Operator" }),
229
173
  );
230
174
 
231
- const srv = await serveSimAws({ simAws, port: 8787 });
232
-
175
+ const server = await serveSimAws({ simAws });
233
176
  const client = new DynamoDBClient({
234
177
  region: simAws.defaultRegionName,
235
- endpoint: `http://localhost:${srv.port}`,
178
+ endpoint: `http://localhost:${server.port}`,
236
179
  credentials: {
237
180
  accessKeyId: created.AccessKey.AccessKeyId,
238
181
  secretAccessKey: created.AccessKey.SecretAccessKey,
@@ -242,885 +185,236 @@ const client = new DynamoDBClient({
242
185
  await client.send(
243
186
  new PutItemCommand({
244
187
  TableName: "widgets",
245
- Item: { id: { S: "w1" } },
188
+ Item: { id: { S: "widget-1" } },
246
189
  }),
247
190
  );
248
191
 
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>
192
+ await server.close();
369
193
  ```
370
194
 
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
- ```
195
+ The endpoint reads the service and Region from the request's SigV4 credential scope. Simulated IAM
196
+ verifies the signature and authorizes the principal that owns the access key. Temporary credentials
197
+ from simulated STS work in the same way.
400
198
 
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:
199
+ An unsigned AWS API request contains no service identity and is refused. Unknown credentials,
200
+ expired sessions, bad signatures, and denied actions return AWS-shaped authentication or
201
+ authorization errors.
402
202
 
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
- ```
203
+ Set `forcePathStyle: true` on an `S3Client` that uses the shared endpoint. The Bucket name must stay
204
+ in the request path because the endpoint hostname is `localhost`.
411
205
 
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.
206
+ The [AWS CLI guide](https://yulinsim.dev/cli/) shows the equivalent environment variables and named
207
+ profile configuration.
413
208
 
414
- Simulated S3 also answers its own Bucket hostnames, covered above. That path is unchanged, and it is what a website visitor uses.
209
+ ## Query simulated DNS
415
210
 
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.
211
+ `serveSimAws` starts a UDP DNS server beside the HTTP server. It usually uses the same port number,
212
+ but UDP may already be using that number. Always read `server.dnsPort`.
417
213
 
418
- ### SNS over the endpoint
214
+ Point a DNS client at `127.0.0.1:<dnsPort>` to resolve records from simulated Route 53. Names that
215
+ route to an HTTP service resolve to `127.0.0.1`, where the local HTTP server is listening. The DNS
216
+ server also answers supported records such as TXT and NS records.
419
217
 
420
- `aws sns` and an `SNSClient` reach simulated SNS through the same endpoint URL:
218
+ For example, with the DNS port set to `8787`:
421
219
 
422
220
  ```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
- });
221
+ dig @127.0.0.1 -p 8787 www.example.test A
538
222
  ```
539
223
 
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.
224
+ The DNS server uses UDP only. `server.close()` releases both the HTTP and DNS ports.
543
225
 
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:
226
+ ## Stop the server
546
227
 
547
- ```typescript sim-serve-close-environment
548
- /**
549
- * Letting go of what an unserved environment is holding.
550
- */
228
+ Always await `server.close()` when a script or test finishes. It closes active connections and any
229
+ file watchers owned by the `SimAws` instance. The simulated resources remain available in memory.
551
230
 
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`:
231
+ Yulin does not install process signal handlers automatically. A development script can ask the
232
+ server to close on `SIGINT` and `SIGTERM`:
570
233
 
571
234
  ```typescript sim-serve-close-on-signal
572
235
  /**
573
- * Asking for the signal handler rather than writing one.
236
+ * Closing a local server when the process receives a termination signal.
574
237
  */
575
238
 
576
239
  import { SimAws } from "@kensio/yulin";
577
240
  import { serveSimAws } from "@kensio/yulin/serve";
578
241
 
579
242
  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.
243
+ const server = await serveSimAws({ simAws, port: 8787 });
583
244
 
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();
245
+ const removeSignalHandlers = server.closeOnSignal();
587
246
 
588
- // A script that stops wanting the handler before the process ends takes it off
589
- // again:
590
- stopListening();
247
+ // Call this only if the script later takes responsibility for signals itself.
248
+ removeSignalHandlers();
249
+ await server.close();
591
250
  ```
592
251
 
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.
252
+ Pass `signals` to `closeOnSignal` to replace the default signal list. The returned function removes
253
+ the installed handlers. `SimAws.closeOnSignal()` provides the same behavior when no local server is
254
+ running.
599
255
 
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.
256
+ A server waits for a pinned HTTP port for up to two seconds during startup. This allows a restarted
257
+ process to take the port after the previous process releases it. Yulin throws
258
+ `SimAwsLocalPortInUse` if the port stays occupied.
603
259
 
604
- ## Live reload
260
+ ## Reload a browser after changes
605
261
 
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:
262
+ Set `liveReload: true` when the local server is serving HTML during development:
610
263
 
611
264
  ```typescript sim-serve-live-reload
612
265
  /**
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.
266
+ * Serving HTML with browser reload support.
716
267
  */
717
268
 
718
- import path from "node:path";
719
-
720
- import { CreateBucketCommand } from "@aws-sdk/client-s3";
721
269
  import { SimAws } from "@kensio/yulin";
722
270
  import { serveSimAws } from "@kensio/yulin/serve";
723
271
 
724
272
  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({
273
+ const server = await serveSimAws({
803
274
  simAws,
804
275
  port: 8787,
805
- messageLogging: { sns: false },
276
+ liveReload: true,
806
277
  });
807
278
 
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:
279
+ // Reload connected browsers after changing simulated content in place.
280
+ server.reload();
817
281
 
818
- ```
819
- sim SNS: SMS to +15550100 (suppressed, number opted out)
820
- Your one-time code is 118221
282
+ await server.close();
821
283
  ```
822
284
 
823
- ### What an email prints as
285
+ The server injects a small client script into an HTML response when all of these conditions hold:
824
286
 
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:
287
+ - the response media type is `text/html`
288
+ - the request accepts `text/html`
289
+ - the request is unsigned and has no `x-amz-*` header
290
+ - the response is unencoded, has a body, and is not a partial response
291
+ - the request method is not `HEAD`
830
292
 
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
- ```
293
+ Signed SDK requests keep their original response bytes. An injected response has
294
+ `x-sim-aws-live-reload: injected`, uses `cache-control: no-store`, and drops validators that no
295
+ longer describe the changed body.
840
296
 
841
- A text part is printed up to 2000 characters. What runs past that is counted and left out:
297
+ The reload channel uses `/__sim-aws/live-reload` on every served hostname. Calling `reload()` while
298
+ live reload is disabled throws an error.
842
299
 
843
- ```
844
- Text body:
845
- Here is your monthly statement in full.
846
- ... 4162 more characters, not printed
847
- ```
300
+ ### Reload after an in-process update
848
301
 
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).
302
+ Pass the server as a reload target when Yulin already watches the changed files. A mounted S3
303
+ directory accepts `{ reload: server }`. A watched CloudFormation template accepts the same target:
852
304
 
853
- `emailTextLimit` moves that limit. It sits beside the per-kind switches on the same option:
854
-
855
- ```typescript sim-serve-email-text-limit
305
+ ```typescript sim-serve-watch-template
856
306
  /**
857
- * Serving with a shorter limit on the email text that reaches the console.
307
+ * Updating a stack and reloading browsers when its template changes.
858
308
  */
859
309
 
860
310
  import { SimAws } from "@kensio/yulin";
861
311
  import { serveSimAws } from "@kensio/yulin/serve";
862
312
 
863
313
  const simAws = new SimAws();
864
- const srv = await serveSimAws({
865
- simAws,
866
- port: 8787,
867
- messageLogging: { emailTextLimit: 500 },
868
- });
314
+ const server = await serveSimAws({ simAws, liveReload: true });
869
315
 
870
- // Serve the pages that send the email here.
316
+ await simAws.cloudFormation().deployTemplateFile({
317
+ templatePath: "cdk.out/TestStack.template.json",
318
+ watch: { reload: server },
319
+ });
871
320
 
872
- await srv.close();
321
+ // Keep serving until the application shuts down.
873
322
  ```
874
323
 
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.
324
+ Yulin waits for the stack update before reloading. A failed update leaves the current page and
325
+ resources in place.
878
326
 
879
- ## Restarting on a file change
327
+ ### Restart after source changes
880
328
 
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:
329
+ Run a development command through `yulin watch` when source changes require a new process:
883
330
 
884
331
  ```bash
885
332
  yulin watch -- tsx dev.ts
886
333
  ```
887
334
 
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).
335
+ The watcher restarts the command after files stop changing for 250 milliseconds. Use
336
+ `--settle=<milliseconds>` for builds that write files in longer bursts. It watches the working
337
+ directory and paths reported by Yulin, including mounted S3 directories and deployed template
338
+ files.
912
339
 
913
- ### One restart for a burst of writes
340
+ Paths handled in process are excluded from restart handling. This includes a template with a
341
+ `watch` option and a mounted directory with a reload target.
914
342
 
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.
343
+ `yulin watch` keeps running when the child command throws. The next file change starts it again. It
344
+ also detects a setup loop where each start writes the file that caused the restart.
918
345
 
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:
346
+ Pass `--inspect=<port>` to add a Node inspector port to each child process:
927
347
 
928
348
  ```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
- });
349
+ yulin watch --inspect=9230 -- tsx dev.ts
970
350
  ```
971
351
 
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.
352
+ The watcher does not run `cdk synth`. Run your own synth command and let Yulin react to the changed
353
+ template file.
984
354
 
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.
355
+ ## Print simulated messages
987
356
 
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).
357
+ While the local server is running, it prints messages recorded by simulated Cognito, SNS, and SES.
358
+ This puts confirmation codes and local-development emails in the terminal.
992
359
 
993
- ### Answering a change instead of restarting
360
+ Use `messageLogging` to disable a message kind or shorten the printed part of an email:
994
361
 
995
- A deployment that watches its own template file is left alone by the supervisor:
996
-
997
- ```typescript sim-serve-watch-template
362
+ ```typescript sim-serve-message-logging
998
363
  /**
999
- * A template the process updates its stack from, rather than restarting for.
364
+ * Disabling SNS message output and limiting printed email text.
1000
365
  */
1001
366
 
1002
367
  import { SimAws } from "@kensio/yulin";
1003
368
  import { serveSimAws } from "@kensio/yulin/serve";
1004
369
 
1005
370
  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,
371
+ const server = await serveSimAws({
372
+ simAws,
373
+ messageLogging: {
374
+ sns: false,
375
+ emailTextLimit: 500,
1012
376
  },
1013
377
  });
1014
- ```
1015
378
 
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
- });
379
+ await server.close();
1061
380
  ```
1062
381
 
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).
382
+ `messageLogging: false` disables all message output. An object leaves every omitted kind enabled.
383
+ Email output includes the sender, recipients, subject, and text body up to the configured limit.
384
+ HTML bodies are reported by size.
1066
385
 
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.
386
+ Only messages recorded while the server is running are printed. The service-specific message
387
+ history remains available through its inspection methods.
1069
388
 
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
- ```
389
+ ## Available functionality
1089
390
 
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.
391
+ - `serveSimAws` exposes simulated service hosts and supported AWS APIs over local HTTP.
392
+ - `SimAwsHttp` handles Fetch API requests in process.
393
+ - `SimAwsDns` handles DNS datagrams in process, and `SimAwsDnsServer` serves them over UDP.
394
+ - `server.localUrl(...)` converts simulated service URLs to the server's local port.
395
+ - `server.close()`, `server.closeOnSignal()`, and `simAws.close()` release network and file-watching
396
+ resources.
397
+ - Live reload supports explicit reloads, watched templates, mounted S3 directories, and process
398
+ restarts.
399
+ - The server logs simulated Cognito messages, SNS text messages, and SES emails by default.
400
+ - The shared AWS API endpoint serves S3, STS, IAM, ELBv2, SNS, CloudFormation, Lambda, and simulated
401
+ services that use the AWS JSON protocol.
1093
402
 
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.
403
+ The AWS JSON services include DynamoDB, DynamoDB Streams, SQS, Cognito Identity Provider,
404
+ EventBridge, ECS, SSM, ACM, CloudWatch, CloudWatch Logs, KMS, Secrets Manager, and Rekognition. Each
405
+ service guide lists its supported commands.
1097
406
 
1098
407
  ## Limitations
1099
408
 
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.
409
+ - The local server speaks HTTP only. `localUrl(...)` changes an HTTPS service URL to HTTP.
410
+ - Only the `Location` header is rewritten for local browsing. URLs in response bodies and cookie
411
+ domains remain unchanged.
412
+ - Live reload changes eligible HTML responses and is disabled by default. It decodes HTML as UTF-8.
413
+ - Live reload reserves `/__sim-aws/live-reload` and has no browser overlay for update failures.
414
+ - The DNS server uses UDP only.
415
+ - `yulin watch` does not preserve simulated state across a restart or reload changed Lambda and
416
+ CloudFront Function code inside the current process.
417
+ - The shared AWS API endpoint refuses unsupported protocols and operations with `501 Not
418
+ Implemented`.
419
+ - Some service functionality is available only through `SimAws` or SDK interception. Check the
420
+ relevant service guide before depending on access through the local endpoint.