local-serverless-stack 1.0.1 → 1.1.0

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 (183) hide show
  1. package/CHANGELOG.md +125 -3
  2. package/README.md +75 -26
  3. package/bin/cli.js +81 -15
  4. package/bin/i18n.js +30 -3
  5. package/dist/client/index.d.ts +1 -1
  6. package/dist/client/index.d.ts.map +1 -1
  7. package/dist/client/namespaces/services.d.ts +46 -0
  8. package/dist/client/namespaces/services.d.ts.map +1 -1
  9. package/dist/client/namespaces/services.js +1 -0
  10. package/dist/client/namespaces/services.js.map +1 -1
  11. package/dist/mcp/tools.d.ts.map +1 -1
  12. package/dist/mcp/tools.js +17 -0
  13. package/dist/mcp/tools.js.map +1 -1
  14. package/dist/server/engine/backends/self-backend.d.ts +26 -1
  15. package/dist/server/engine/backends/self-backend.d.ts.map +1 -1
  16. package/dist/server/engine/backends/self-backend.js +104 -11
  17. package/dist/server/engine/backends/self-backend.js.map +1 -1
  18. package/dist/server/engine/emulators/dynamodb/expressions/parser.d.ts.map +1 -1
  19. package/dist/server/engine/emulators/dynamodb/expressions/parser.js +12 -1
  20. package/dist/server/engine/emulators/dynamodb/expressions/parser.js.map +1 -1
  21. package/dist/server/engine/emulators/dynamodb/expressions/reserved-words.d.ts +3 -0
  22. package/dist/server/engine/emulators/dynamodb/expressions/reserved-words.d.ts.map +1 -0
  23. package/dist/server/engine/emulators/dynamodb/expressions/reserved-words.js +78 -0
  24. package/dist/server/engine/emulators/dynamodb/expressions/reserved-words.js.map +1 -0
  25. package/dist/server/engine/emulators/dynamodb/index.d.ts +11 -0
  26. package/dist/server/engine/emulators/dynamodb/index.d.ts.map +1 -1
  27. package/dist/server/engine/emulators/dynamodb/index.js +101 -7
  28. package/dist/server/engine/emulators/dynamodb/index.js.map +1 -1
  29. package/dist/server/engine/emulators/dynamodb/query.d.ts +1 -0
  30. package/dist/server/engine/emulators/dynamodb/query.d.ts.map +1 -1
  31. package/dist/server/engine/emulators/dynamodb/query.js +34 -6
  32. package/dist/server/engine/emulators/dynamodb/query.js.map +1 -1
  33. package/dist/server/engine/emulators/s3/index.d.ts +10 -0
  34. package/dist/server/engine/emulators/s3/index.d.ts.map +1 -1
  35. package/dist/server/engine/emulators/s3/index.js +369 -25
  36. package/dist/server/engine/emulators/s3/index.js.map +1 -1
  37. package/dist/server/engine/emulators/sqs/index.d.ts +2 -0
  38. package/dist/server/engine/emulators/sqs/index.d.ts.map +1 -1
  39. package/dist/server/engine/emulators/sqs/index.js +70 -7
  40. package/dist/server/engine/emulators/sqs/index.js.map +1 -1
  41. package/dist/server/engine/emulators/sqs/queue.d.ts +2 -1
  42. package/dist/server/engine/emulators/sqs/queue.d.ts.map +1 -1
  43. package/dist/server/engine/emulators/sqs/queue.js +68 -35
  44. package/dist/server/engine/emulators/sqs/queue.js.map +1 -1
  45. package/dist/server/engine/emulators/ssm/index.d.ts +31 -0
  46. package/dist/server/engine/emulators/ssm/index.d.ts.map +1 -0
  47. package/dist/server/engine/emulators/ssm/index.js +670 -0
  48. package/dist/server/engine/emulators/ssm/index.js.map +1 -0
  49. package/dist/server/engine/http/errors.d.ts +1 -0
  50. package/dist/server/engine/http/errors.d.ts.map +1 -1
  51. package/dist/server/engine/http/errors.js +13 -5
  52. package/dist/server/engine/http/errors.js.map +1 -1
  53. package/dist/server/engine/http/is-aws-request.d.ts.map +1 -1
  54. package/dist/server/engine/http/is-aws-request.js +13 -3
  55. package/dist/server/engine/http/is-aws-request.js.map +1 -1
  56. package/dist/server/engine/http/protocols/aws-json.d.ts +1 -0
  57. package/dist/server/engine/http/protocols/aws-json.d.ts.map +1 -1
  58. package/dist/server/engine/http/protocols/aws-json.js +13 -0
  59. package/dist/server/engine/http/protocols/aws-json.js.map +1 -1
  60. package/dist/server/engine/http/reset.d.ts +4 -0
  61. package/dist/server/engine/http/reset.d.ts.map +1 -0
  62. package/dist/server/engine/http/reset.js +17 -0
  63. package/dist/server/engine/http/reset.js.map +1 -0
  64. package/dist/server/engine/http/router.d.ts +1 -0
  65. package/dist/server/engine/http/router.d.ts.map +1 -1
  66. package/dist/server/engine/http/router.js +185 -34
  67. package/dist/server/engine/http/router.js.map +1 -1
  68. package/dist/server/engine/types.d.ts +2 -1
  69. package/dist/server/engine/types.d.ts.map +1 -1
  70. package/dist/server/routes/apis.d.ts.map +1 -1
  71. package/dist/server/routes/apis.js +6 -0
  72. package/dist/server/routes/apis.js.map +1 -1
  73. package/dist/server/routes/config.d.ts.map +1 -1
  74. package/dist/server/routes/config.js +19 -1
  75. package/dist/server/routes/config.js.map +1 -1
  76. package/dist/server/routes/services.d.ts.map +1 -1
  77. package/dist/server/routes/services.js +49 -1
  78. package/dist/server/routes/services.js.map +1 -1
  79. package/dist/server/runtime/runtime-worker.js +33 -3
  80. package/dist/server/runtime/runtime-worker.js.map +1 -1
  81. package/dist/server/services/api-gateway-events.d.ts +14 -0
  82. package/dist/server/services/api-gateway-events.d.ts.map +1 -1
  83. package/dist/server/services/api-gateway-events.js +114 -8
  84. package/dist/server/services/api-gateway-events.js.map +1 -1
  85. package/dist/server/services/artifact-staleness.d.ts +20 -0
  86. package/dist/server/services/artifact-staleness.d.ts.map +1 -0
  87. package/dist/server/services/artifact-staleness.js +115 -0
  88. package/dist/server/services/artifact-staleness.js.map +1 -0
  89. package/dist/server/services/authorizer-service.d.ts +34 -5
  90. package/dist/server/services/authorizer-service.d.ts.map +1 -1
  91. package/dist/server/services/authorizer-service.js +0 -0
  92. package/dist/server/services/authorizer-service.js.map +1 -1
  93. package/dist/server/services/cfn-intrinsics.d.ts +63 -0
  94. package/dist/server/services/cfn-intrinsics.d.ts.map +1 -0
  95. package/dist/server/services/cfn-intrinsics.js +403 -0
  96. package/dist/server/services/cfn-intrinsics.js.map +1 -0
  97. package/dist/server/services/cfn-physical-values.d.ts +51 -0
  98. package/dist/server/services/cfn-physical-values.d.ts.map +1 -0
  99. package/dist/server/services/cfn-physical-values.js +186 -0
  100. package/dist/server/services/cfn-physical-values.js.map +1 -0
  101. package/dist/server/services/cloudformation-parser.d.ts +26 -3
  102. package/dist/server/services/cloudformation-parser.d.ts.map +1 -1
  103. package/dist/server/services/cloudformation-parser.js +76 -4
  104. package/dist/server/services/cloudformation-parser.js.map +1 -1
  105. package/dist/server/services/config-manager.d.ts +24 -2
  106. package/dist/server/services/config-manager.d.ts.map +1 -1
  107. package/dist/server/services/config-manager.js +60 -7
  108. package/dist/server/services/config-manager.js.map +1 -1
  109. package/dist/server/services/gateway-manager.d.ts.map +1 -1
  110. package/dist/server/services/gateway-manager.js +52 -20
  111. package/dist/server/services/gateway-manager.js.map +1 -1
  112. package/dist/server/services/lambda-runtime-manager.d.ts +1 -0
  113. package/dist/server/services/lambda-runtime-manager.d.ts.map +1 -1
  114. package/dist/server/services/lambda-runtime-manager.js +36 -14
  115. package/dist/server/services/lambda-runtime-manager.js.map +1 -1
  116. package/dist/server/services/raw-api-assembler.d.ts +1 -1
  117. package/dist/server/services/raw-api-assembler.d.ts.map +1 -1
  118. package/dist/server/services/raw-api-assembler.js +18 -8
  119. package/dist/server/services/raw-api-assembler.js.map +1 -1
  120. package/dist/server/services/resource-provisioner.d.ts +12 -0
  121. package/dist/server/services/resource-provisioner.d.ts.map +1 -1
  122. package/dist/server/services/resource-provisioner.js +128 -13
  123. package/dist/server/services/resource-provisioner.js.map +1 -1
  124. package/dist/server/services/serverless-state-parser.d.ts +45 -2
  125. package/dist/server/services/serverless-state-parser.d.ts.map +1 -1
  126. package/dist/server/services/serverless-state-parser.js +217 -29
  127. package/dist/server/services/serverless-state-parser.js.map +1 -1
  128. package/dist/server/services/service-graph.d.ts +76 -0
  129. package/dist/server/services/service-graph.d.ts.map +1 -0
  130. package/dist/server/services/service-graph.js +914 -0
  131. package/dist/server/services/service-graph.js.map +1 -0
  132. package/dist/server/services/service-registrar.d.ts +4 -0
  133. package/dist/server/services/service-registrar.d.ts.map +1 -1
  134. package/dist/server/services/service-registrar.js +129 -55
  135. package/dist/server/services/service-registrar.js.map +1 -1
  136. package/dist/server/services/source-watcher.d.ts +1 -0
  137. package/dist/server/services/source-watcher.d.ts.map +1 -1
  138. package/dist/server/services/source-watcher.js +19 -1
  139. package/dist/server/services/source-watcher.js.map +1 -1
  140. package/dist/server/services/worker-runtime.d.ts +82 -0
  141. package/dist/server/services/worker-runtime.d.ts.map +1 -0
  142. package/dist/server/services/worker-runtime.js +303 -0
  143. package/dist/server/services/worker-runtime.js.map +1 -0
  144. package/dist/ui/assets/{ApisPage-CTOcNna-.js → ApisPage-BkW-QZ0X.js} +1 -1
  145. package/dist/ui/assets/BucketDetailPage-CJwTQJh7.js +1 -0
  146. package/dist/ui/assets/BucketsPage-B1xPVhZC.js +1 -0
  147. package/dist/ui/assets/{DynamoPage-BnFFKnIU.js → DynamoPage-DvbpwZDc.js} +1 -1
  148. package/dist/ui/assets/DynamoTablePage-xn3Lq3so.js +3 -0
  149. package/dist/ui/assets/LambdaDetailPage-BhUyO77H.js +4 -0
  150. package/dist/ui/assets/{LambdasPage-Bg-MWFBb.js → LambdasPage-CUF_4rOH.js} +1 -1
  151. package/dist/ui/assets/OnboardingPage-U49THUzu.js +1 -0
  152. package/dist/ui/assets/OpenSearchCollectionPage-BJTVNCq4.js +1 -0
  153. package/dist/ui/assets/OpenSearchPage-4s-egkB9.js +1 -0
  154. package/dist/ui/assets/OverviewPage-DJeJQktG.js +1 -0
  155. package/dist/ui/assets/{OverviewPage-CSWY71vZ.css → OverviewPage-DjQETmb_.css} +1 -1
  156. package/dist/ui/assets/QueueDetailPage-DWsy5Yt8.js +1 -0
  157. package/dist/ui/assets/QueuesPage-BZJSqman.js +1 -0
  158. package/dist/ui/assets/SecretsPage-YC9ua54G.js +1 -0
  159. package/dist/ui/assets/ServiceDetailPage-Cum5frP4.js +2 -0
  160. package/dist/ui/assets/ServicesPage-IpaPDuqx.js +2 -0
  161. package/dist/ui/assets/SettingsPage-BiCRU8O6.js +1 -0
  162. package/dist/ui/assets/index-BDlrAJhi.css +1 -0
  163. package/dist/ui/assets/index-BouKSS43.js +38 -0
  164. package/dist/ui/assets/resourceIcons-DZs28nbs.js +1 -0
  165. package/dist/ui/index.html +5 -2
  166. package/package.json +1 -1
  167. package/dist/ui/assets/BucketDetailPage-BXW8-FrE.js +0 -1
  168. package/dist/ui/assets/BucketsPage-DY_ITepb.js +0 -1
  169. package/dist/ui/assets/DynamoTablePage-BLCuyJHU.js +0 -3
  170. package/dist/ui/assets/LambdaDetailPage-D4rZNLRT.js +0 -4
  171. package/dist/ui/assets/OnboardingPage-nnl313Fm.js +0 -1
  172. package/dist/ui/assets/OpenSearchCollectionPage-Dn15lH_F.js +0 -1
  173. package/dist/ui/assets/OpenSearchPage-CgD5isok.js +0 -1
  174. package/dist/ui/assets/OverviewPage-m5sUr8IN.js +0 -1
  175. package/dist/ui/assets/QueueDetailPage-Ch_FDDoN.js +0 -1
  176. package/dist/ui/assets/QueuesPage-C_yDuGDu.js +0 -1
  177. package/dist/ui/assets/SecretsPage-BSGMSoqy.js +0 -1
  178. package/dist/ui/assets/ServiceDetailPage-6KyohyjI.js +0 -2
  179. package/dist/ui/assets/ServicesPage-BbSetOMo.js +0 -2
  180. package/dist/ui/assets/SettingsPage-BgF1jFr6.js +0 -1
  181. package/dist/ui/assets/index-D627ZyYU.css +0 -1
  182. package/dist/ui/assets/index-DuSPnI19.js +0 -37
  183. package/dist/ui/assets/resourceIcons-baecADXA.js +0 -1
@@ -13,6 +13,83 @@ const OWNER_ID = '75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba0
13
13
  const OWNER_DISPLAY_NAME = 'webfile';
14
14
  const DEFAULT_CONTENT_TYPE = 'binary/octet-stream';
15
15
  const METADATA_HEADER_PREFIX = 'x-amz-meta-';
16
+ const SSE_ALGORITHMS = ['AES256', 'aws:kms', 'aws:kms:dsse'];
17
+ // Since January 2023 AWS applies SSE-S3 (AES256) to every bucket that has no
18
+ // explicit default-encryption rule, and GetBucketEncryption reports that rule
19
+ // instead of the old ServerSideEncryptionConfigurationNotFoundError — so a
20
+ // bucket without a stored configuration (and every bucket persisted before
21
+ // this field existed) answers with this default.
22
+ const DEFAULT_BUCKET_ENCRYPTION = { sseAlgorithm: 'AES256', bucketKeyEnabled: false };
23
+ // Since April 2023 every new AWS bucket is created with all four Block Public
24
+ // Access settings on, and GetPublicAccessBlock reports them. Only an explicit
25
+ // DeletePublicAccessBlock makes the call answer
26
+ // NoSuchPublicAccessBlockConfiguration.
27
+ const DEFAULT_PUBLIC_ACCESS_BLOCK = {
28
+ blockPublicAcls: true,
29
+ ignorePublicAcls: true,
30
+ blockPublicPolicy: true,
31
+ restrictPublicBuckets: true,
32
+ };
33
+ // Request header (PutObject / CopyObject REPLACE) ↔ record field for the
34
+ // stored system metadata. The same names are accepted as presigned-POST form
35
+ // fields, and objectHeaders() emits them under these canonical spellings.
36
+ const SYSTEM_METADATA_HEADERS = [
37
+ ['contentDisposition', 'Content-Disposition'],
38
+ ['cacheControl', 'Cache-Control'],
39
+ ['contentEncoding', 'Content-Encoding'],
40
+ ['contentLanguage', 'Content-Language'],
41
+ ['expires', 'Expires'],
42
+ ];
43
+ const SSE_HEADER = 'x-amz-server-side-encryption';
44
+ const SSE_KMS_KEY_HEADER = 'x-amz-server-side-encryption-aws-kms-key-id';
45
+ const SSE_BUCKET_KEY_HEADER = 'x-amz-server-side-encryption-bucket-key-enabled';
46
+ // Query keys that turn a bucket request into a subresource operation, mapped
47
+ // to the stem of the AWS operation name (Put/Get/Delete + stem). A request
48
+ // carrying one of these must NEVER fall through to CreateBucket/DeleteBucket:
49
+ // a PutBucketTagging answered as CreateBucket is a 409, and a
50
+ // DeleteBucketTagging answered as DeleteBucket destroys the (empty) bucket.
51
+ // Keys the engine does not serve answer NotImplemented (501, as AWS does for
52
+ // an operation it does not support) — see handleBucketSubresource().
53
+ const BUCKET_SUBRESOURCES = {
54
+ accelerate: 'BucketAccelerateConfiguration',
55
+ acl: 'BucketAcl',
56
+ analytics: 'BucketAnalyticsConfiguration',
57
+ cors: 'BucketCors',
58
+ delete: 'Objects',
59
+ encryption: 'BucketEncryption',
60
+ 'intelligent-tiering': 'BucketIntelligentTieringConfiguration',
61
+ inventory: 'BucketInventoryConfiguration',
62
+ lifecycle: 'BucketLifecycleConfiguration',
63
+ location: 'BucketLocation',
64
+ logging: 'BucketLogging',
65
+ metadataTable: 'BucketMetadataTableConfiguration',
66
+ metrics: 'BucketMetricsConfiguration',
67
+ notification: 'BucketNotificationConfiguration',
68
+ 'object-lock': 'ObjectLockConfiguration',
69
+ ownershipControls: 'BucketOwnershipControls',
70
+ policy: 'BucketPolicy',
71
+ policyStatus: 'BucketPolicyStatus',
72
+ publicAccessBlock: 'PublicAccessBlock',
73
+ replication: 'BucketReplication',
74
+ requestPayment: 'BucketRequestPayment',
75
+ tagging: 'BucketTagging',
76
+ versioning: 'BucketVersioning',
77
+ versions: 'ObjectVersions',
78
+ website: 'BucketWebsite',
79
+ };
80
+ // Same guard for objects: PUT /bucket/key?tagging must not overwrite the
81
+ // object with the tagging XML, DELETE ...?tagging must not delete it, and
82
+ // GET ...?acl must not return the body. None of these is served yet.
83
+ const OBJECT_SUBRESOURCES = {
84
+ acl: 'ObjectAcl',
85
+ attributes: 'ObjectAttributes',
86
+ 'legal-hold': 'ObjectLegalHold',
87
+ restore: 'Object',
88
+ retention: 'ObjectRetention',
89
+ select: 'ObjectContent',
90
+ tagging: 'ObjectTagging',
91
+ torrent: 'ObjectTorrent',
92
+ };
16
93
  export class S3Emulator {
17
94
  service = 's3';
18
95
  ctx;
@@ -107,24 +184,14 @@ export class S3Emulator {
107
184
  // Bucket-level operations
108
185
  // -------------------------------------------------------------------------
109
186
  async handleBucket(req, bucket) {
110
- if (req.method === 'PUT') {
111
- if ('versioning' in req.query)
112
- return this.putBucketVersioning(bucket, req);
113
- if ('notification' in req.query)
114
- return this.putBucketNotification(bucket, req);
115
- if ('cors' in req.query)
116
- return this.putBucketCors(bucket, req);
187
+ const subresource = findSubresource(req.query, BUCKET_SUBRESOURCES);
188
+ if (subresource !== undefined)
189
+ return this.handleBucketSubresource(req, bucket, subresource);
190
+ // From here on the request carries no subresource: a bare PUT is
191
+ // CreateBucket and a bare DELETE is DeleteBucket.
192
+ if (req.method === 'PUT')
117
193
  return this.createBucket(bucket, req);
118
- }
119
194
  if (req.method === 'GET') {
120
- if ('location' in req.query)
121
- return this.getBucketLocation(bucket);
122
- if ('versioning' in req.query)
123
- return this.getBucketVersioning(bucket);
124
- if ('notification' in req.query)
125
- return this.getBucketNotification(bucket);
126
- if ('cors' in req.query)
127
- return this.getBucketCors(bucket);
128
195
  if (req.query['list-type'] === '2')
129
196
  return this.listObjectsV2(bucket, req.query);
130
197
  throw notImplementedOperation('s3', 'ListObjects (v1) — request with list-type=2');
@@ -133,19 +200,75 @@ export class S3Emulator {
133
200
  this.requireBucket(bucket);
134
201
  return { status: 200 };
135
202
  }
136
- if (req.method === 'DELETE') {
137
- if ('cors' in req.query)
138
- return this.deleteBucketCors(bucket);
203
+ if (req.method === 'DELETE')
139
204
  return this.deleteBucket(bucket);
140
- }
141
- if (req.method === 'POST' && 'delete' in req.query)
142
- return this.deleteObjects(bucket, req);
143
205
  // Browser form upload (presigned POST): multipart/form-data to the bucket.
144
206
  if (req.method === 'POST' && multipartBoundary(req.headers['content-type'])) {
145
207
  return this.postObject(bucket, req);
146
208
  }
147
209
  throw notImplementedOperation('s3', `${req.method} /<bucket>`);
148
210
  }
211
+ async handleBucketSubresource(req, bucket, subresource) {
212
+ switch (`${req.method} ${subresource}`) {
213
+ case 'GET location':
214
+ return this.getBucketLocation(bucket);
215
+ case 'PUT versioning':
216
+ return this.putBucketVersioning(bucket, req);
217
+ case 'GET versioning':
218
+ return this.getBucketVersioning(bucket);
219
+ case 'PUT notification':
220
+ return this.putBucketNotification(bucket, req);
221
+ case 'GET notification':
222
+ return this.getBucketNotification(bucket);
223
+ case 'PUT cors':
224
+ return this.putBucketCors(bucket, req);
225
+ case 'GET cors':
226
+ return this.getBucketCors(bucket);
227
+ case 'DELETE cors':
228
+ return this.deleteBucketCors(bucket);
229
+ case 'POST delete':
230
+ return this.deleteObjects(bucket, req);
231
+ case 'PUT encryption':
232
+ return this.putBucketEncryption(bucket, req);
233
+ case 'GET encryption':
234
+ return this.getBucketEncryption(bucket);
235
+ case 'DELETE encryption':
236
+ return this.updateBucket(bucket, { encryption: undefined });
237
+ case 'PUT tagging':
238
+ return this.putBucketTagging(bucket, req);
239
+ case 'GET tagging':
240
+ return this.getBucketTagging(bucket);
241
+ case 'DELETE tagging':
242
+ return this.updateBucket(bucket, { tags: undefined });
243
+ case 'PUT publicAccessBlock':
244
+ return this.putPublicAccessBlock(bucket, req);
245
+ case 'GET publicAccessBlock':
246
+ return this.getPublicAccessBlock(bucket);
247
+ case 'DELETE publicAccessBlock':
248
+ return this.updateBucket(bucket, { publicAccessBlock: null });
249
+ case 'PUT policy':
250
+ return this.putBucketPolicy(bucket, req);
251
+ case 'GET policy':
252
+ return this.getBucketPolicy(bucket);
253
+ case 'DELETE policy':
254
+ return this.updateBucket(bucket, { policy: undefined });
255
+ default:
256
+ throw subresourceNotImplemented(req.method, subresource, BUCKET_SUBRESOURCES);
257
+ }
258
+ }
259
+ // Shared tail of the Delete* configuration calls: AWS answers 204 even when
260
+ // nothing was configured. Fields set to undefined are dropped from the
261
+ // persisted record; DeleteBucketEncryption therefore reverts the bucket to
262
+ // the SSE-S3 default, exactly like AWS.
263
+ updateBucket(bucket, changes) {
264
+ const record = { ...this.requireBucket(bucket), ...changes };
265
+ for (const field of Object.keys(changes)) {
266
+ if (record[field] === undefined)
267
+ delete record[field];
268
+ }
269
+ this.buckets.set(bucket, record);
270
+ return { status: 204 };
271
+ }
149
272
  createBucket(bucket, req) {
150
273
  if (this.buckets.get(bucket)) {
151
274
  throw s3Error('BucketAlreadyOwnedByYou', 'Your previous request to create the named bucket succeeded and you already own it.', 409);
@@ -236,6 +359,85 @@ export class S3Emulator {
236
359
  this.buckets.set(bucket, rest);
237
360
  return { status: 204 };
238
361
  }
362
+ putBucketEncryption(bucket, req) {
363
+ const record = this.requireBucket(bucket);
364
+ this.buckets.set(bucket, { ...record, encryption: parseEncryptionConfiguration(req.body) });
365
+ return { status: 200 };
366
+ }
367
+ getBucketEncryption(bucket) {
368
+ const { encryption = DEFAULT_BUCKET_ENCRYPTION } = this.requireBucket(bucket);
369
+ const byDefault = tag('SSEAlgorithm', encryption.sseAlgorithm) +
370
+ (encryption.kmsKeyId !== undefined ? tag('KMSMasterKeyID', encryption.kmsKeyId) : '');
371
+ const rule = tag('Rule', tag('ApplyServerSideEncryptionByDefault', byDefault, { escape: false }) +
372
+ tag('BucketKeyEnabled', encryption.bucketKeyEnabled ? 'true' : 'false'), { escape: false });
373
+ return xmlResponse(200, tag('ServerSideEncryptionConfiguration', rule, { attrs: { xmlns: S3_XMLNS }, escape: false }));
374
+ }
375
+ putBucketTagging(bucket, req) {
376
+ const record = this.requireBucket(bucket);
377
+ const tags = parseTagging(req.body);
378
+ const { tags: previous, ...untagged } = record;
379
+ void previous;
380
+ // An empty TagSet leaves the bucket untagged — Get then answers NoSuchTagSet.
381
+ this.buckets.set(bucket, tags.length > 0 ? { ...untagged, tags } : untagged);
382
+ return { status: 204 };
383
+ }
384
+ getBucketTagging(bucket) {
385
+ const { tags } = this.requireBucket(bucket);
386
+ if (!tags)
387
+ throw s3Error('NoSuchTagSet', 'The TagSet does not exist', 404);
388
+ const tagXml = tags.map(t => tag('Tag', tag('Key', t.key) + tag('Value', t.value), { escape: false })).join('');
389
+ return xmlResponse(200, tag('Tagging', tag('TagSet', tagXml, { escape: false }), { attrs: { xmlns: S3_XMLNS }, escape: false }));
390
+ }
391
+ putPublicAccessBlock(bucket, req) {
392
+ const record = this.requireBucket(bucket);
393
+ const root = parseBodyXml(req.body);
394
+ // Omitted flags are false, as in AWS (the SDK only serializes set members).
395
+ const flag = (name) => (childText(root, name) ?? '').toLowerCase() === 'true';
396
+ this.buckets.set(bucket, {
397
+ ...record,
398
+ publicAccessBlock: {
399
+ blockPublicAcls: flag('BlockPublicAcls'),
400
+ ignorePublicAcls: flag('IgnorePublicAcls'),
401
+ blockPublicPolicy: flag('BlockPublicPolicy'),
402
+ restrictPublicBuckets: flag('RestrictPublicBuckets'),
403
+ },
404
+ });
405
+ return { status: 200 };
406
+ }
407
+ getPublicAccessBlock(bucket) {
408
+ const { publicAccessBlock = DEFAULT_PUBLIC_ACCESS_BLOCK } = this.requireBucket(bucket);
409
+ if (publicAccessBlock === null) {
410
+ throw s3Error('NoSuchPublicAccessBlockConfiguration', 'The public access block configuration was not found', 404);
411
+ }
412
+ const content = tag('BlockPublicAcls', String(publicAccessBlock.blockPublicAcls)) +
413
+ tag('IgnorePublicAcls', String(publicAccessBlock.ignorePublicAcls)) +
414
+ tag('BlockPublicPolicy', String(publicAccessBlock.blockPublicPolicy)) +
415
+ tag('RestrictPublicBuckets', String(publicAccessBlock.restrictPublicBuckets));
416
+ return xmlResponse(200, tag('PublicAccessBlockConfiguration', content, { attrs: { xmlns: S3_XMLNS }, escape: false }));
417
+ }
418
+ // The policy is stored verbatim (AWS re-serializes it, but SDKs and IaC
419
+ // compare it as parsed JSON). It is never evaluated: the engine does not
420
+ // authorize requests.
421
+ putBucketPolicy(bucket, req) {
422
+ const record = this.requireBucket(bucket);
423
+ const policy = req.body.toString('utf8');
424
+ try {
425
+ const parsed = JSON.parse(policy);
426
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
427
+ throw new Error('not an object');
428
+ }
429
+ catch {
430
+ throw s3Error('MalformedPolicy', 'Policies must be valid JSON and the first byte must be \'{\'', 400);
431
+ }
432
+ this.buckets.set(bucket, { ...record, policy });
433
+ return { status: 204 };
434
+ }
435
+ getBucketPolicy(bucket) {
436
+ const { policy } = this.requireBucket(bucket);
437
+ if (policy === undefined)
438
+ throw s3Error('NoSuchBucketPolicy', 'The bucket policy does not exist', 404);
439
+ return { status: 200, headers: { 'Content-Type': 'application/json' }, body: policy };
440
+ }
239
441
  async listObjectsV2(bucket, query) {
240
442
  this.requireBucket(bucket);
241
443
  const index = await this.index(bucket);
@@ -339,6 +541,9 @@ export class S3Emulator {
339
541
  // Object-level operations
340
542
  // -------------------------------------------------------------------------
341
543
  async handleObject(req, bucket, key) {
544
+ const subresource = findSubresource(req.query, OBJECT_SUBRESOURCES);
545
+ if (subresource !== undefined)
546
+ throw subresourceNotImplemented(req.method, subresource, OBJECT_SUBRESOURCES);
342
547
  if (req.method === 'PUT') {
343
548
  if (req.headers['x-amz-copy-source'])
344
549
  return this.copyObject(bucket, key, req);
@@ -366,14 +571,16 @@ export class S3Emulator {
366
571
  blobPath,
367
572
  contentType: req.headers['content-type'] ?? DEFAULT_CONTENT_TYPE,
368
573
  metadata: metadataFromHeaders(req.headers),
574
+ ...systemMetadataFromHeaders(req.headers),
369
575
  storageClass: 'STANDARD',
576
+ sse: resolveSse(name => req.headers[name], bucketRecord),
370
577
  };
371
578
  index.put(key, record);
372
579
  if (previous && previous.blobPath !== blobPath) {
373
580
  await this.deleteBlobIfUnreferenced(index, previous.blobPath);
374
581
  }
375
582
  this.queueObjectEvent(bucketRecord.region, bucket, 's3:ObjectCreated:Put', key, record.size, eTag);
376
- return { status: 200, headers: { ETag: `"${eTag}"` } };
583
+ return { status: 200, headers: { ETag: `"${eTag}"`, ...sseHeaders(record) } };
377
584
  }
378
585
  // Presigned POST (browser form upload): the object key and options travel as
379
586
  // form fields, the bytes as a `file` part. The POST policy/signature fields
@@ -397,6 +604,8 @@ export class S3Emulator {
397
604
  const key = keyTemplate.replace(/\$\{filename\}/g, () => uploadName);
398
605
  if (!key)
399
606
  throw s3Error('InvalidArgument', 'The specified key is not valid.', 400);
607
+ // Validate the SSE fields before any byte is written.
608
+ const sse = resolveSse(lookup, bucketRecord);
400
609
  const index = await this.index(bucket);
401
610
  const eTag = md5Hex(file.data);
402
611
  const blobPath = await this.ctx.store.writeBlob(`s3/${bucket}/blobs`, file.data);
@@ -408,14 +617,19 @@ export class S3Emulator {
408
617
  blobPath,
409
618
  contentType: lookup('content-type') ?? file.contentType ?? DEFAULT_CONTENT_TYPE,
410
619
  metadata: metadataFromFields(fields),
620
+ // Same names as the PutObject headers, as form fields (case-insensitive).
621
+ ...systemMetadataFrom((_field, header) => lookup(header)),
411
622
  storageClass: 'STANDARD',
623
+ sse,
412
624
  };
413
625
  index.put(key, record);
414
626
  if (previous && previous.blobPath !== blobPath) {
415
627
  await this.deleteBlobIfUnreferenced(index, previous.blobPath);
416
628
  }
417
629
  this.queueObjectEvent(bucketRecord.region, bucket, 's3:ObjectCreated:Post', key, record.size, eTag);
418
- return this.postResponse(bucket, key, eTag, fields);
630
+ const response = this.postResponse(bucket, key, eTag, fields);
631
+ response.headers = { ...response.headers, ...sseHeaders(record) };
632
+ return response;
419
633
  }
420
634
  postResponse(bucket, key, eTag, fields) {
421
635
  const value = (name) => fields.find(field => field.name.toLowerCase() === name.toLowerCase() && field.filename === undefined)?.data.toString('utf8');
@@ -458,6 +672,10 @@ export class S3Emulator {
458
672
  const sourceRecord = sourceIndex.get(sourceKey);
459
673
  if (!sourceRecord)
460
674
  throw s3Error('NoSuchKey', 'The specified key does not exist.', 404);
675
+ // Encryption is not metadata: whatever the directive, the copy is
676
+ // encrypted per the request's SSE headers or, absent those, the
677
+ // DESTINATION bucket's default — never inherited from the source (AWS).
678
+ const sse = resolveSse(name => req.headers[name], bucketRecord);
461
679
  const data = await this.ctx.store.readBlob(sourceRecord.blobPath);
462
680
  const index = await this.index(bucket);
463
681
  const blobPath = await this.ctx.store.writeBlob(`s3/${bucket}/blobs`, data);
@@ -471,7 +689,11 @@ export class S3Emulator {
471
689
  blobPath,
472
690
  contentType: replaceMetadata ? req.headers['content-type'] ?? DEFAULT_CONTENT_TYPE : sourceRecord.contentType,
473
691
  metadata: replaceMetadata ? metadataFromHeaders(req.headers) : { ...sourceRecord.metadata },
692
+ // System metadata follows the directive like user metadata does: COPY
693
+ // keeps the source's, REPLACE takes only what the request carries.
694
+ ...(replaceMetadata ? systemMetadataFromHeaders(req.headers) : systemMetadataFrom(field => sourceRecord[field])),
474
695
  storageClass: 'STANDARD',
696
+ sse,
475
697
  };
476
698
  index.put(key, record);
477
699
  if (previous && previous.blobPath !== blobPath) {
@@ -482,7 +704,9 @@ export class S3Emulator {
482
704
  attrs: { xmlns: S3_XMLNS },
483
705
  escape: false,
484
706
  });
485
- return xmlResponse(200, result);
707
+ const response = xmlResponse(200, result);
708
+ response.headers = { ...response.headers, ...sseHeaders(record) };
709
+ return response;
486
710
  }
487
711
  async getObject(bucket, key, req) {
488
712
  const record = await this.requireObject(bucket, key);
@@ -634,18 +858,138 @@ function metadataFromFields(fields) {
634
858
  }
635
859
  return metadata;
636
860
  }
861
+ // Collect the stored system metadata (Content-Disposition, Cache-Control, …)
862
+ // through a lookup keyed by record field and canonical header name; empty
863
+ // values are dropped, as S3 does not store an empty header.
864
+ function systemMetadataFrom(lookup) {
865
+ const metadata = {};
866
+ for (const [field, header] of SYSTEM_METADATA_HEADERS) {
867
+ const value = lookup(field, header);
868
+ if (value)
869
+ metadata[field] = value;
870
+ }
871
+ return metadata;
872
+ }
873
+ function systemMetadataFromHeaders(headers) {
874
+ return systemMetadataFrom((_field, header) => headers[header.toLowerCase()]);
875
+ }
876
+ // Encryption for a new object (PutObject, presigned POST, CopyObject). The
877
+ // lookup is keyed by the lowercase x-amz-* name (a request header, or a POST
878
+ // form field of the same name). With no SSE header the bucket's default
879
+ // encryption applies — SSE-S3 unless PutBucketEncryption set another rule —
880
+ // which is what AWS reports for every object since 2023.
881
+ function resolveSse(lookup, bucket) {
882
+ const algorithm = lookup(SSE_HEADER);
883
+ const kmsKeyId = lookup(SSE_KMS_KEY_HEADER);
884
+ const bucketKey = lookup(SSE_BUCKET_KEY_HEADER);
885
+ if (algorithm && !SSE_ALGORITHMS.includes(algorithm)) {
886
+ throw s3Error('InvalidArgument', 'The encryption method specified is not supported', 400);
887
+ }
888
+ if (kmsKeyId && (!algorithm || algorithm === 'AES256')) {
889
+ throw s3Error('InvalidArgument', `Server Side Encryption with AWS KMS managed key requires HTTP header ${SSE_HEADER} : aws:kms`, 400);
890
+ }
891
+ if (!algorithm) {
892
+ const { sseAlgorithm, kmsKeyId: defaultKey, bucketKeyEnabled } = bucket.encryption ?? DEFAULT_BUCKET_ENCRYPTION;
893
+ if (sseAlgorithm === 'AES256')
894
+ return { sseAlgorithm };
895
+ return { sseAlgorithm, ...(defaultKey !== undefined ? { kmsKeyId: defaultKey } : {}), bucketKeyEnabled: bucketKeyEnabled === true };
896
+ }
897
+ const sseAlgorithm = algorithm;
898
+ if (sseAlgorithm === 'AES256')
899
+ return { sseAlgorithm };
900
+ // An explicit aws:kms without a key id means the AWS managed key (aws/s3)
901
+ // — not the bucket default's key — so no key id is recorded. S3 Bucket
902
+ // Keys fall back to the bucket-level setting when the header is omitted.
903
+ return {
904
+ sseAlgorithm,
905
+ ...(kmsKeyId ? { kmsKeyId } : {}),
906
+ bucketKeyEnabled: bucketKey !== undefined ? bucketKey.toLowerCase() === 'true' : bucket.encryption?.bucketKeyEnabled === true,
907
+ };
908
+ }
909
+ function sseHeaders(record) {
910
+ const sse = record.sse ?? DEFAULT_BUCKET_ENCRYPTION;
911
+ const headers = { [SSE_HEADER]: sse.sseAlgorithm };
912
+ if (sse.kmsKeyId !== undefined)
913
+ headers[SSE_KMS_KEY_HEADER] = sse.kmsKeyId;
914
+ if (sse.bucketKeyEnabled)
915
+ headers[SSE_BUCKET_KEY_HEADER] = 'true';
916
+ return headers;
917
+ }
637
918
  function objectHeaders(record) {
638
919
  const headers = {
639
920
  'Content-Type': record.contentType,
640
921
  ETag: `"${record.eTag}"`,
641
922
  'Last-Modified': new Date(record.lastModified).toUTCString(),
642
923
  'Accept-Ranges': 'bytes',
924
+ ...sseHeaders(record),
643
925
  };
926
+ for (const [field, header] of SYSTEM_METADATA_HEADERS) {
927
+ const value = record[field];
928
+ if (value !== undefined)
929
+ headers[header] = value;
930
+ }
644
931
  for (const [name, value] of Object.entries(record.metadata)) {
645
932
  headers[`${METADATA_HEADER_PREFIX}${name}`] = value;
646
933
  }
647
934
  return headers;
648
935
  }
936
+ // First query key naming a known subresource (own keys only, so a query
937
+ // param such as `constructor` never matches an Object.prototype member).
938
+ function findSubresource(query, known) {
939
+ return Object.keys(query).find(key => Object.prototype.hasOwnProperty.call(known, key));
940
+ }
941
+ // Operations whose AWS name is not Verb + stem.
942
+ const IRREGULAR_OPERATION_NAMES = {
943
+ 'GET versions': 'ListObjectVersions',
944
+ 'POST restore': 'RestoreObject',
945
+ 'POST select': 'SelectObjectContent',
946
+ };
947
+ function subresourceNotImplemented(method, subresource, known) {
948
+ const verb = method.charAt(0) + method.slice(1).toLowerCase();
949
+ const operation = IRREGULAR_OPERATION_NAMES[`${method} ${subresource}`] ?? `${verb}${known[subresource]}`;
950
+ return s3Error('NotImplemented', `s3.${operation} (${method} ?${subresource}) is not implemented by the LSS self engine — see docs/SELF_ENGINE.md#coverage`, 501);
951
+ }
952
+ // PutBucketEncryption body. AWS accepts exactly one Rule; the engine keeps
953
+ // the first and rejects a rule without a supported SSEAlgorithm the way AWS
954
+ // does (MalformedXML).
955
+ function parseEncryptionConfiguration(body) {
956
+ const root = parseBodyXml(body);
957
+ const rule = childrenNamed(root, 'Rule')[0];
958
+ const byDefault = rule && childrenNamed(rule, 'ApplyServerSideEncryptionByDefault')[0];
959
+ const algorithm = byDefault && childText(byDefault, 'SSEAlgorithm');
960
+ if (!algorithm || !SSE_ALGORITHMS.includes(algorithm)) {
961
+ throw s3Error('MalformedXML', 'The XML you provided was not well-formed or did not validate against our published schema', 400);
962
+ }
963
+ const kmsKeyId = childText(byDefault, 'KMSMasterKeyID');
964
+ if (kmsKeyId && algorithm === 'AES256') {
965
+ throw s3Error('InvalidArgument', 'a KMSMasterKeyID is not applicable if the default sse algorithm is not aws:kms or aws:kms:dsse', 400);
966
+ }
967
+ return {
968
+ sseAlgorithm: algorithm,
969
+ ...(kmsKeyId ? { kmsKeyId } : {}),
970
+ bucketKeyEnabled: (childText(rule, 'BucketKeyEnabled') ?? '').toLowerCase() === 'true',
971
+ };
972
+ }
973
+ // PutBucketTagging body: <Tagging><TagSet><Tag><Key/><Value/></Tag>…</TagSet>.
974
+ function parseTagging(body) {
975
+ const root = parseBodyXml(body);
976
+ const tagSet = childrenNamed(root, 'TagSet')[0];
977
+ if (!tagSet) {
978
+ throw s3Error('MalformedXML', 'The XML you provided was not well-formed or did not validate against our published schema', 400);
979
+ }
980
+ const tags = [];
981
+ const seen = new Set();
982
+ for (const node of childrenNamed(tagSet, 'Tag')) {
983
+ const key = childText(node, 'Key');
984
+ if (!key)
985
+ throw s3Error('InvalidTag', 'The TagKey you have provided is invalid', 400);
986
+ if (seen.has(key))
987
+ throw s3Error('InvalidTag', 'Cannot provide multiple Tags with the same key', 400);
988
+ seen.add(key);
989
+ tags.push({ key, value: childText(node, 'Value') ?? '' });
990
+ }
991
+ return tags;
992
+ }
649
993
  // GET/HEAD response-header overrides carried as `response-*` query params (the
650
994
  // presigned-URL mechanism for forcing a download filename / content type). AWS
651
995
  // echoes each present param into the matching response header — this is what a