@constructive-io/bucket-provisioner 0.30.0 → 0.31.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.
package/README.md CHANGED
@@ -224,7 +224,8 @@ CORS rules for public/temp buckets (PUT, GET, HEAD).
224
224
 
225
225
  #### `buildPrivateCorsRules(allowedOrigins, maxAgeSeconds?)`
226
226
 
227
- CORS rules for private buckets (PUT, HEAD only — no GET).
227
+ CORS rules for private buckets (PUT, GET, HEAD for the configured origins).
228
+ Presigned downloads still require browser CORS; allowing GET does not grant unsigned object access.
228
229
 
229
230
  ### Lifecycle Builders
230
231
 
@@ -273,7 +274,7 @@ Error codes:
273
274
 
274
275
  | Access Type | Block Public Access | Bucket Policy | CORS Methods | Lifecycle |
275
276
  |-------------|-------------------|---------------|--------------|-----------|
276
- | `private` | All blocked | None (deleted) | PUT, HEAD | None |
277
+ | `private` | All blocked | None (deleted) | PUT, GET, HEAD | None |
277
278
  | `public` | Partially relaxed | Public-read | PUT, GET, HEAD | None |
278
279
  | `temp` | All blocked | None (deleted) | PUT, GET, HEAD | Auto-expire (1 day) |
279
280
 
package/cors.d.ts CHANGED
@@ -23,9 +23,9 @@ export declare function buildUploadCorsRules(allowedOrigins: string[], maxAgeSec
23
23
  /**
24
24
  * Build restrictive CORS rules for private-only buckets.
25
25
  *
26
- * Similar to upload CORS but without GET (private files use
27
- * presigned URLs which include auth in the query string,
28
- * so CORS is less of a concern for downloads).
26
+ * Presigned GETs still need CORS for browser fetch/preview. Allow reads from
27
+ * the configured origins; S3 authorization and public-access blocking still
28
+ * reject unsigned access to private objects.
29
29
  *
30
30
  * @param allowedOrigins - Domains allowed to make cross-origin requests.
31
31
  * @param maxAgeSeconds - Preflight cache duration (default: 3600 = 1 hour)
package/cors.js CHANGED
@@ -53,9 +53,9 @@ function buildUploadCorsRules(allowedOrigins, maxAgeSeconds = 3600) {
53
53
  /**
54
54
  * Build restrictive CORS rules for private-only buckets.
55
55
  *
56
- * Similar to upload CORS but without GET (private files use
57
- * presigned URLs which include auth in the query string,
58
- * so CORS is less of a concern for downloads).
56
+ * Presigned GETs still need CORS for browser fetch/preview. Allow reads from
57
+ * the configured origins; S3 authorization and public-access blocking still
58
+ * reject unsigned access to private objects.
59
59
  *
60
60
  * @param allowedOrigins - Domains allowed to make cross-origin requests.
61
61
  * @param maxAgeSeconds - Preflight cache duration (default: 3600 = 1 hour)
@@ -67,7 +67,7 @@ function buildPrivateCorsRules(allowedOrigins, maxAgeSeconds = 3600) {
67
67
  return [
68
68
  {
69
69
  allowedOrigins,
70
- allowedMethods: ['PUT', 'HEAD'],
70
+ allowedMethods: ['PUT', 'GET', 'HEAD'],
71
71
  allowedHeaders: [
72
72
  'Content-Type',
73
73
  'Content-Length',
package/esm/cors.d.ts CHANGED
@@ -23,9 +23,9 @@ export declare function buildUploadCorsRules(allowedOrigins: string[], maxAgeSec
23
23
  /**
24
24
  * Build restrictive CORS rules for private-only buckets.
25
25
  *
26
- * Similar to upload CORS but without GET (private files use
27
- * presigned URLs which include auth in the query string,
28
- * so CORS is less of a concern for downloads).
26
+ * Presigned GETs still need CORS for browser fetch/preview. Allow reads from
27
+ * the configured origins; S3 authorization and public-access blocking still
28
+ * reject unsigned access to private objects.
29
29
  *
30
30
  * @param allowedOrigins - Domains allowed to make cross-origin requests.
31
31
  * @param maxAgeSeconds - Preflight cache duration (default: 3600 = 1 hour)
package/esm/cors.js CHANGED
@@ -49,9 +49,9 @@ export function buildUploadCorsRules(allowedOrigins, maxAgeSeconds = 3600) {
49
49
  /**
50
50
  * Build restrictive CORS rules for private-only buckets.
51
51
  *
52
- * Similar to upload CORS but without GET (private files use
53
- * presigned URLs which include auth in the query string,
54
- * so CORS is less of a concern for downloads).
52
+ * Presigned GETs still need CORS for browser fetch/preview. Allow reads from
53
+ * the configured origins; S3 authorization and public-access blocking still
54
+ * reject unsigned access to private objects.
55
55
  *
56
56
  * @param allowedOrigins - Domains allowed to make cross-origin requests.
57
57
  * @param maxAgeSeconds - Preflight cache duration (default: 3600 = 1 hour)
@@ -63,7 +63,7 @@ export function buildPrivateCorsRules(allowedOrigins, maxAgeSeconds = 3600) {
63
63
  return [
64
64
  {
65
65
  allowedOrigins,
66
- allowedMethods: ['PUT', 'HEAD'],
66
+ allowedMethods: ['PUT', 'GET', 'HEAD'],
67
67
  allowedHeaders: [
68
68
  'Content-Type',
69
69
  'Content-Length',
@@ -91,17 +91,16 @@ export declare class BucketProvisioner {
91
91
  /**
92
92
  * Configure S3 Block Public Access settings.
93
93
  *
94
- * Gracefully skips if the S3-compatible backend (e.g. MinIO) does not
95
- * support PutPublicAccessBlock — the operation is best-effort since
96
- * not all providers implement this AWS-specific API.
94
+ * Skips only when the S3-compatible backend (e.g. MinIO) answers that it
95
+ * does not implement PutPublicAccessBlock; every other failure throws.
97
96
  */
98
97
  setPublicAccessBlock(bucketName: string, config: PublicAccessBlockConfig): Promise<void>;
99
98
  /**
100
99
  * Apply an S3 bucket policy.
101
100
  *
102
- * Gracefully skips if the S3-compatible backend does not support
103
- * PutBucketPolicy — bucket policies are best-effort since not all
104
- * providers implement this AWS-specific API.
101
+ * Skips only when the S3-compatible backend answers that it does not
102
+ * implement PutBucketPolicy; every other failure (AccessDenied, a blocked
103
+ * public policy, a malformed document) throws.
105
104
  */
106
105
  setBucketPolicy(bucketName: string, policy: BucketPolicyDocument): Promise<void>;
107
106
  /**
@@ -114,25 +113,27 @@ export declare class BucketProvisioner {
114
113
  /**
115
114
  * Set CORS configuration on an S3 bucket.
116
115
  *
117
- * Gracefully skips if the S3-compatible backend (e.g. older MinIO) does
118
- * not support PutBucketCors — CORS is best-effort since not all providers
119
- * implement this API via the same endpoint path.
116
+ * Skips only when the S3-compatible backend (e.g. older MinIO) answers
117
+ * that it does not implement PutBucketCors; every other failure throws.
120
118
  */
121
119
  setCors(bucketName: string, rules: CorsRule[]): Promise<void>;
120
+ /**
121
+ * Remove the CORS configuration from an S3 bucket, so no browser origin may
122
+ * reach it. A bucket that has none already is the converged state.
123
+ */
124
+ clearCors(bucketName: string): Promise<void>;
122
125
  /**
123
126
  * Enable versioning on an S3 bucket.
124
127
  *
125
- * Gracefully skips if the S3-compatible backend (e.g. MinIO edge-cicd)
126
- * does not support PutBucketVersioning — versioning is best-effort
127
- * since not all providers implement this API.
128
+ * Skips only when the S3-compatible backend (e.g. MinIO edge-cicd)
129
+ * answers that it does not implement PutBucketVersioning.
128
130
  */
129
131
  enableVersioning(bucketName: string): Promise<void>;
130
132
  /**
131
133
  * Set lifecycle rules on an S3 bucket.
132
134
  *
133
- * Gracefully skips if the S3-compatible backend (e.g. MinIO edge-cicd)
134
- * does not support PutBucketLifecycleConfiguration — lifecycle rules
135
- * are best-effort since not all providers implement this API.
135
+ * Skips only when the S3-compatible backend (e.g. MinIO edge-cicd)
136
+ * answers that it does not implement PutBucketLifecycleConfiguration.
136
137
  */
137
138
  setLifecycleRules(bucketName: string, rules: LifecycleRule[]): Promise<void>;
138
139
  /**
@@ -9,12 +9,27 @@
9
9
  * - Private/temp buckets: Block All Public Access, no bucket policy, presigned URLs only
10
10
  * - Public buckets: Block Public Access partially relaxed, public-read bucket policy applied
11
11
  */
12
- import { CreateBucketCommand, DeleteBucketPolicyCommand, GetBucketCorsCommand, GetBucketLifecycleConfigurationCommand, GetBucketPolicyCommand, GetBucketVersioningCommand, GetPublicAccessBlockCommand, HeadBucketCommand, PutBucketCorsCommand, PutBucketLifecycleConfigurationCommand, PutBucketPolicyCommand, PutBucketVersioningCommand, PutPublicAccessBlockCommand, } from '@aws-sdk/client-s3';
12
+ import { CreateBucketCommand, DeleteBucketCorsCommand, DeleteBucketPolicyCommand, GetBucketCorsCommand, GetBucketLifecycleConfigurationCommand, GetBucketPolicyCommand, GetBucketVersioningCommand, GetPublicAccessBlockCommand, HeadBucketCommand, PutBucketCorsCommand, PutBucketLifecycleConfigurationCommand, PutBucketPolicyCommand, PutBucketVersioningCommand, PutPublicAccessBlockCommand, } from '@aws-sdk/client-s3';
13
13
  import { createS3Client } from './client';
14
14
  import { buildPrivateCorsRules, buildUploadCorsRules } from './cors';
15
15
  import { buildTempCleanupRule } from './lifecycle';
16
16
  import { buildPublicReadPolicy, getPublicAccessBlock } from './policies';
17
17
  import { ProvisionerError } from './types';
18
+ /**
19
+ * Error codes an S3-compatible backend answers with when it does not implement
20
+ * a bucket-configuration API at all (older MinIO, some RustFS builds). Matched
21
+ * on the error's code only — never on message text, which AWS fills with the
22
+ * operation and policy names of real failures (an AccessDenied for
23
+ * `s3:PutBucketPolicy` reads "...because no identity-based policy allows...").
24
+ */
25
+ const UNSUPPORTED_OPERATION_CODES = ['XmlParseException', 'NotImplemented'];
26
+ function errorCode(err) {
27
+ return err?.Code ?? err?.name;
28
+ }
29
+ function isUnsupportedOperation(err, extraCodes = []) {
30
+ const code = errorCode(err);
31
+ return code !== undefined && [...UNSUPPORTED_OPERATION_CODES, ...extraCodes].includes(code);
32
+ }
18
33
  /**
19
34
  * The BucketProvisioner handles creating and configuring S3-compatible
20
35
  * buckets with the correct privacy settings, CORS rules, and policies
@@ -91,6 +106,8 @@ export class BucketProvisioner {
91
106
  // A bucket no browser origin may reach gets no rule set: cross-origin
92
107
  // access is the bucket's own recorded data, and "none recorded" is a
93
108
  // legitimate answer, not a missing setting to invent an origin for.
109
+ // Provisioning converges, so "none" also clears a rule set an earlier
110
+ // provision wrote for origins the bucket no longer records.
94
111
  const effectiveOrigins = options.allowedOrigins ?? this.allowedOrigins;
95
112
  const corsRules = effectiveOrigins.length === 0
96
113
  ? []
@@ -100,6 +117,9 @@ export class BucketProvisioner {
100
117
  if (corsRules.length > 0) {
101
118
  await this.setCors(bucketName, corsRules);
102
119
  }
120
+ else {
121
+ await this.clearCors(bucketName);
122
+ }
103
123
  // 5. Versioning
104
124
  if (versioning) {
105
125
  await this.enableVersioning(bucketName);
@@ -173,9 +193,8 @@ export class BucketProvisioner {
173
193
  /**
174
194
  * Configure S3 Block Public Access settings.
175
195
  *
176
- * Gracefully skips if the S3-compatible backend (e.g. MinIO) does not
177
- * support PutPublicAccessBlock — the operation is best-effort since
178
- * not all providers implement this AWS-specific API.
196
+ * Skips only when the S3-compatible backend (e.g. MinIO) answers that it
197
+ * does not implement PutPublicAccessBlock; every other failure throws.
179
198
  */
180
199
  async setPublicAccessBlock(bucketName, config) {
181
200
  try {
@@ -185,16 +204,9 @@ export class BucketProvisioner {
185
204
  }));
186
205
  }
187
206
  catch (err) {
188
- // Some S3-compatible backends (e.g. older MinIO) don't support
189
- // PutPublicAccessBlock. Treat XML parse errors or "not implemented"
190
- // responses as non-fatal — the bucket is still usable.
191
- if (err.Code === 'XmlParseException' ||
192
- err.name === 'XmlParseException' ||
193
- err.Code === 'NotImplemented' ||
194
- err.name === 'NotImplemented' ||
195
- err.message?.includes('not well-formed') ||
196
- err.message?.includes('not implemented') ||
197
- err.message?.includes('PublicAccessBlockConfiguration')) {
207
+ // Some S3-compatible backends (e.g. older MinIO) don't implement
208
+ // PutPublicAccessBlock; the bucket is still usable without it.
209
+ if (isUnsupportedOperation(err)) {
198
210
  return;
199
211
  }
200
212
  throw new ProvisionerError('POLICY_FAILED', `Failed to set public access block on '${bucketName}': ${err.message}`, err);
@@ -203,9 +215,9 @@ export class BucketProvisioner {
203
215
  /**
204
216
  * Apply an S3 bucket policy.
205
217
  *
206
- * Gracefully skips if the S3-compatible backend does not support
207
- * PutBucketPolicy — bucket policies are best-effort since not all
208
- * providers implement this AWS-specific API.
218
+ * Skips only when the S3-compatible backend answers that it does not
219
+ * implement PutBucketPolicy; every other failure (AccessDenied, a blocked
220
+ * public policy, a malformed document) throws.
209
221
  */
210
222
  async setBucketPolicy(bucketName, policy) {
211
223
  try {
@@ -215,15 +227,8 @@ export class BucketProvisioner {
215
227
  }));
216
228
  }
217
229
  catch (err) {
218
- // Some S3-compatible backends don't fully support bucket policies.
219
- // Treat XML parse errors or "not implemented" responses as non-fatal.
220
- if (err.Code === 'XmlParseException' ||
221
- err.name === 'XmlParseException' ||
222
- err.Code === 'NotImplemented' ||
223
- err.name === 'NotImplemented' ||
224
- err.message?.includes('not well-formed') ||
225
- err.message?.includes('not implemented') ||
226
- err.message?.includes('policy')) {
230
+ // Some S3-compatible backends don't implement bucket policies.
231
+ if (isUnsupportedOperation(err)) {
227
232
  return;
228
233
  }
229
234
  throw new ProvisionerError('POLICY_FAILED', `Failed to set bucket policy on '${bucketName}': ${err.message}`, err);
@@ -241,14 +246,9 @@ export class BucketProvisioner {
241
246
  }
242
247
  catch (err) {
243
248
  // No policy to delete — that's fine
244
- if (err.name === 'NoSuchBucketPolicy' ||
249
+ if (errorCode(err) === 'NoSuchBucketPolicy' ||
245
250
  err.$metadata?.httpStatusCode === 404 ||
246
- err.Code === 'XmlParseException' ||
247
- err.name === 'XmlParseException' ||
248
- err.Code === 'NotImplemented' ||
249
- err.name === 'NotImplemented' ||
250
- err.message?.includes('not well-formed') ||
251
- err.message?.includes('not implemented')) {
251
+ isUnsupportedOperation(err)) {
252
252
  return;
253
253
  }
254
254
  throw new ProvisionerError('POLICY_FAILED', `Failed to delete bucket policy on '${bucketName}': ${err.message}`, err);
@@ -257,9 +257,8 @@ export class BucketProvisioner {
257
257
  /**
258
258
  * Set CORS configuration on an S3 bucket.
259
259
  *
260
- * Gracefully skips if the S3-compatible backend (e.g. older MinIO) does
261
- * not support PutBucketCors — CORS is best-effort since not all providers
262
- * implement this API via the same endpoint path.
260
+ * Skips only when the S3-compatible backend (e.g. older MinIO) answers
261
+ * that it does not implement PutBucketCors; every other failure throws.
263
262
  */
264
263
  async setCors(bucketName, rules) {
265
264
  try {
@@ -277,27 +276,34 @@ export class BucketProvisioner {
277
276
  }));
278
277
  }
279
278
  catch (err) {
280
- // Some S3-compatible backends (e.g. older MinIO) don't support
281
- // PutBucketCors via the standard path. Treat XML parse errors or
282
- // "not implemented" responses as non-fatal.
283
- if (err.Code === 'XmlParseException' ||
284
- err.name === 'XmlParseException' ||
285
- err.Code === 'NotImplemented' ||
286
- err.name === 'NotImplemented' ||
287
- err.message?.includes('not well-formed') ||
288
- err.message?.includes('not implemented') ||
289
- err.message?.includes('CORSConfiguration')) {
279
+ // Some S3-compatible backends (e.g. older MinIO) don't implement
280
+ // PutBucketCors.
281
+ if (isUnsupportedOperation(err)) {
290
282
  return;
291
283
  }
292
284
  throw new ProvisionerError('CORS_FAILED', `Failed to set CORS on '${bucketName}': ${err.message}`, err);
293
285
  }
294
286
  }
287
+ /**
288
+ * Remove the CORS configuration from an S3 bucket, so no browser origin may
289
+ * reach it. A bucket that has none already is the converged state.
290
+ */
291
+ async clearCors(bucketName) {
292
+ try {
293
+ await this.client.send(new DeleteBucketCorsCommand({ Bucket: bucketName }));
294
+ }
295
+ catch (err) {
296
+ if (errorCode(err) === 'NoSuchCORSConfiguration' || isUnsupportedOperation(err)) {
297
+ return;
298
+ }
299
+ throw new ProvisionerError('CORS_FAILED', `Failed to clear CORS on '${bucketName}': ${err.message}`, err);
300
+ }
301
+ }
295
302
  /**
296
303
  * Enable versioning on an S3 bucket.
297
304
  *
298
- * Gracefully skips if the S3-compatible backend (e.g. MinIO edge-cicd)
299
- * does not support PutBucketVersioning — versioning is best-effort
300
- * since not all providers implement this API.
305
+ * Skips only when the S3-compatible backend (e.g. MinIO edge-cicd)
306
+ * answers that it does not implement PutBucketVersioning.
301
307
  */
302
308
  async enableVersioning(bucketName) {
303
309
  try {
@@ -307,16 +313,9 @@ export class BucketProvisioner {
307
313
  }));
308
314
  }
309
315
  catch (err) {
310
- // Some S3-compatible backends (e.g. MinIO edge-cicd) don't support
311
- // PutBucketVersioning. Treat XML parse errors or "not implemented"
312
- // responses as non-fatal.
313
- if (err.Code === 'XmlParseException' ||
314
- err.name === 'XmlParseException' ||
315
- err.Code === 'NotImplemented' ||
316
- err.name === 'NotImplemented' ||
317
- err.message?.includes('not well-formed') ||
318
- err.message?.includes('not implemented') ||
319
- err.message?.includes('VersioningConfiguration')) {
316
+ // Some S3-compatible backends (e.g. MinIO edge-cicd) don't implement
317
+ // PutBucketVersioning.
318
+ if (isUnsupportedOperation(err)) {
320
319
  return;
321
320
  }
322
321
  throw new ProvisionerError('VERSIONING_FAILED', `Failed to enable versioning on '${bucketName}': ${err.message}`, err);
@@ -325,9 +324,8 @@ export class BucketProvisioner {
325
324
  /**
326
325
  * Set lifecycle rules on an S3 bucket.
327
326
  *
328
- * Gracefully skips if the S3-compatible backend (e.g. MinIO edge-cicd)
329
- * does not support PutBucketLifecycleConfiguration — lifecycle rules
330
- * are best-effort since not all providers implement this API.
327
+ * Skips only when the S3-compatible backend (e.g. MinIO edge-cicd)
328
+ * answers that it does not implement PutBucketLifecycleConfiguration.
331
329
  */
332
330
  async setLifecycleRules(bucketName, rules) {
333
331
  try {
@@ -344,20 +342,10 @@ export class BucketProvisioner {
344
342
  }));
345
343
  }
346
344
  catch (err) {
347
- // Some S3-compatible backends (e.g. MinIO edge-cicd) don't support
348
- // PutBucketLifecycleConfiguration (may require Content-MD5 header
349
- // the SDK doesn't send). Treat XML parse errors, "not implemented",
350
- // or missing-header responses as non-fatal.
351
- if (err.Code === 'XmlParseException' ||
352
- err.name === 'XmlParseException' ||
353
- err.Code === 'NotImplemented' ||
354
- err.name === 'NotImplemented' ||
355
- err.Code === 'MissingContentMD5' ||
356
- err.name === 'MissingContentMD5' ||
357
- err.message?.includes('not well-formed') ||
358
- err.message?.includes('not implemented') ||
359
- err.message?.includes('Content-Md5') ||
360
- err.message?.includes('LifecycleConfiguration')) {
345
+ // Some S3-compatible backends (e.g. MinIO edge-cicd) don't implement
346
+ // PutBucketLifecycleConfiguration, or require a Content-MD5 header the
347
+ // SDK doesn't send.
348
+ if (isUnsupportedOperation(err, ['MissingContentMD5'])) {
361
349
  return;
362
350
  }
363
351
  throw new ProvisionerError('LIFECYCLE_FAILED', `Failed to set lifecycle rules on '${bucketName}': ${err.message}`, err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructive-io/bucket-provisioner",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "S3-compatible bucket provisioning library — create buckets, configure privacy policies, CORS, and access controls",
6
6
  "main": "index.js",
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@aws-sdk/client-s3": "^3.1052.0",
33
- "@constructive-io/s3-utils": "^2.34.0"
33
+ "@constructive-io/s3-utils": "^2.35.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "makage": "^0.8.0"
@@ -44,5 +44,5 @@
44
44
  "storage",
45
45
  "constructive"
46
46
  ],
47
- "gitHead": "1f3501d518e175803a1a3372cf9249922d82bfe5"
47
+ "gitHead": "487c4ec9ee997d7da80584a3d41d7120d4354c54"
48
48
  }
package/provisioner.d.ts CHANGED
@@ -91,17 +91,16 @@ export declare class BucketProvisioner {
91
91
  /**
92
92
  * Configure S3 Block Public Access settings.
93
93
  *
94
- * Gracefully skips if the S3-compatible backend (e.g. MinIO) does not
95
- * support PutPublicAccessBlock — the operation is best-effort since
96
- * not all providers implement this AWS-specific API.
94
+ * Skips only when the S3-compatible backend (e.g. MinIO) answers that it
95
+ * does not implement PutPublicAccessBlock; every other failure throws.
97
96
  */
98
97
  setPublicAccessBlock(bucketName: string, config: PublicAccessBlockConfig): Promise<void>;
99
98
  /**
100
99
  * Apply an S3 bucket policy.
101
100
  *
102
- * Gracefully skips if the S3-compatible backend does not support
103
- * PutBucketPolicy — bucket policies are best-effort since not all
104
- * providers implement this AWS-specific API.
101
+ * Skips only when the S3-compatible backend answers that it does not
102
+ * implement PutBucketPolicy; every other failure (AccessDenied, a blocked
103
+ * public policy, a malformed document) throws.
105
104
  */
106
105
  setBucketPolicy(bucketName: string, policy: BucketPolicyDocument): Promise<void>;
107
106
  /**
@@ -114,25 +113,27 @@ export declare class BucketProvisioner {
114
113
  /**
115
114
  * Set CORS configuration on an S3 bucket.
116
115
  *
117
- * Gracefully skips if the S3-compatible backend (e.g. older MinIO) does
118
- * not support PutBucketCors — CORS is best-effort since not all providers
119
- * implement this API via the same endpoint path.
116
+ * Skips only when the S3-compatible backend (e.g. older MinIO) answers
117
+ * that it does not implement PutBucketCors; every other failure throws.
120
118
  */
121
119
  setCors(bucketName: string, rules: CorsRule[]): Promise<void>;
120
+ /**
121
+ * Remove the CORS configuration from an S3 bucket, so no browser origin may
122
+ * reach it. A bucket that has none already is the converged state.
123
+ */
124
+ clearCors(bucketName: string): Promise<void>;
122
125
  /**
123
126
  * Enable versioning on an S3 bucket.
124
127
  *
125
- * Gracefully skips if the S3-compatible backend (e.g. MinIO edge-cicd)
126
- * does not support PutBucketVersioning — versioning is best-effort
127
- * since not all providers implement this API.
128
+ * Skips only when the S3-compatible backend (e.g. MinIO edge-cicd)
129
+ * answers that it does not implement PutBucketVersioning.
128
130
  */
129
131
  enableVersioning(bucketName: string): Promise<void>;
130
132
  /**
131
133
  * Set lifecycle rules on an S3 bucket.
132
134
  *
133
- * Gracefully skips if the S3-compatible backend (e.g. MinIO edge-cicd)
134
- * does not support PutBucketLifecycleConfiguration — lifecycle rules
135
- * are best-effort since not all providers implement this API.
135
+ * Skips only when the S3-compatible backend (e.g. MinIO edge-cicd)
136
+ * answers that it does not implement PutBucketLifecycleConfiguration.
136
137
  */
137
138
  setLifecycleRules(bucketName: string, rules: LifecycleRule[]): Promise<void>;
138
139
  /**
package/provisioner.js CHANGED
@@ -18,6 +18,21 @@ const cors_1 = require("./cors");
18
18
  const lifecycle_1 = require("./lifecycle");
19
19
  const policies_1 = require("./policies");
20
20
  const types_1 = require("./types");
21
+ /**
22
+ * Error codes an S3-compatible backend answers with when it does not implement
23
+ * a bucket-configuration API at all (older MinIO, some RustFS builds). Matched
24
+ * on the error's code only — never on message text, which AWS fills with the
25
+ * operation and policy names of real failures (an AccessDenied for
26
+ * `s3:PutBucketPolicy` reads "...because no identity-based policy allows...").
27
+ */
28
+ const UNSUPPORTED_OPERATION_CODES = ['XmlParseException', 'NotImplemented'];
29
+ function errorCode(err) {
30
+ return err?.Code ?? err?.name;
31
+ }
32
+ function isUnsupportedOperation(err, extraCodes = []) {
33
+ const code = errorCode(err);
34
+ return code !== undefined && [...UNSUPPORTED_OPERATION_CODES, ...extraCodes].includes(code);
35
+ }
21
36
  /**
22
37
  * The BucketProvisioner handles creating and configuring S3-compatible
23
38
  * buckets with the correct privacy settings, CORS rules, and policies
@@ -94,6 +109,8 @@ class BucketProvisioner {
94
109
  // A bucket no browser origin may reach gets no rule set: cross-origin
95
110
  // access is the bucket's own recorded data, and "none recorded" is a
96
111
  // legitimate answer, not a missing setting to invent an origin for.
112
+ // Provisioning converges, so "none" also clears a rule set an earlier
113
+ // provision wrote for origins the bucket no longer records.
97
114
  const effectiveOrigins = options.allowedOrigins ?? this.allowedOrigins;
98
115
  const corsRules = effectiveOrigins.length === 0
99
116
  ? []
@@ -103,6 +120,9 @@ class BucketProvisioner {
103
120
  if (corsRules.length > 0) {
104
121
  await this.setCors(bucketName, corsRules);
105
122
  }
123
+ else {
124
+ await this.clearCors(bucketName);
125
+ }
106
126
  // 5. Versioning
107
127
  if (versioning) {
108
128
  await this.enableVersioning(bucketName);
@@ -176,9 +196,8 @@ class BucketProvisioner {
176
196
  /**
177
197
  * Configure S3 Block Public Access settings.
178
198
  *
179
- * Gracefully skips if the S3-compatible backend (e.g. MinIO) does not
180
- * support PutPublicAccessBlock — the operation is best-effort since
181
- * not all providers implement this AWS-specific API.
199
+ * Skips only when the S3-compatible backend (e.g. MinIO) answers that it
200
+ * does not implement PutPublicAccessBlock; every other failure throws.
182
201
  */
183
202
  async setPublicAccessBlock(bucketName, config) {
184
203
  try {
@@ -188,16 +207,9 @@ class BucketProvisioner {
188
207
  }));
189
208
  }
190
209
  catch (err) {
191
- // Some S3-compatible backends (e.g. older MinIO) don't support
192
- // PutPublicAccessBlock. Treat XML parse errors or "not implemented"
193
- // responses as non-fatal — the bucket is still usable.
194
- if (err.Code === 'XmlParseException' ||
195
- err.name === 'XmlParseException' ||
196
- err.Code === 'NotImplemented' ||
197
- err.name === 'NotImplemented' ||
198
- err.message?.includes('not well-formed') ||
199
- err.message?.includes('not implemented') ||
200
- err.message?.includes('PublicAccessBlockConfiguration')) {
210
+ // Some S3-compatible backends (e.g. older MinIO) don't implement
211
+ // PutPublicAccessBlock; the bucket is still usable without it.
212
+ if (isUnsupportedOperation(err)) {
201
213
  return;
202
214
  }
203
215
  throw new types_1.ProvisionerError('POLICY_FAILED', `Failed to set public access block on '${bucketName}': ${err.message}`, err);
@@ -206,9 +218,9 @@ class BucketProvisioner {
206
218
  /**
207
219
  * Apply an S3 bucket policy.
208
220
  *
209
- * Gracefully skips if the S3-compatible backend does not support
210
- * PutBucketPolicy — bucket policies are best-effort since not all
211
- * providers implement this AWS-specific API.
221
+ * Skips only when the S3-compatible backend answers that it does not
222
+ * implement PutBucketPolicy; every other failure (AccessDenied, a blocked
223
+ * public policy, a malformed document) throws.
212
224
  */
213
225
  async setBucketPolicy(bucketName, policy) {
214
226
  try {
@@ -218,15 +230,8 @@ class BucketProvisioner {
218
230
  }));
219
231
  }
220
232
  catch (err) {
221
- // Some S3-compatible backends don't fully support bucket policies.
222
- // Treat XML parse errors or "not implemented" responses as non-fatal.
223
- if (err.Code === 'XmlParseException' ||
224
- err.name === 'XmlParseException' ||
225
- err.Code === 'NotImplemented' ||
226
- err.name === 'NotImplemented' ||
227
- err.message?.includes('not well-formed') ||
228
- err.message?.includes('not implemented') ||
229
- err.message?.includes('policy')) {
233
+ // Some S3-compatible backends don't implement bucket policies.
234
+ if (isUnsupportedOperation(err)) {
230
235
  return;
231
236
  }
232
237
  throw new types_1.ProvisionerError('POLICY_FAILED', `Failed to set bucket policy on '${bucketName}': ${err.message}`, err);
@@ -244,14 +249,9 @@ class BucketProvisioner {
244
249
  }
245
250
  catch (err) {
246
251
  // No policy to delete — that's fine
247
- if (err.name === 'NoSuchBucketPolicy' ||
252
+ if (errorCode(err) === 'NoSuchBucketPolicy' ||
248
253
  err.$metadata?.httpStatusCode === 404 ||
249
- err.Code === 'XmlParseException' ||
250
- err.name === 'XmlParseException' ||
251
- err.Code === 'NotImplemented' ||
252
- err.name === 'NotImplemented' ||
253
- err.message?.includes('not well-formed') ||
254
- err.message?.includes('not implemented')) {
254
+ isUnsupportedOperation(err)) {
255
255
  return;
256
256
  }
257
257
  throw new types_1.ProvisionerError('POLICY_FAILED', `Failed to delete bucket policy on '${bucketName}': ${err.message}`, err);
@@ -260,9 +260,8 @@ class BucketProvisioner {
260
260
  /**
261
261
  * Set CORS configuration on an S3 bucket.
262
262
  *
263
- * Gracefully skips if the S3-compatible backend (e.g. older MinIO) does
264
- * not support PutBucketCors — CORS is best-effort since not all providers
265
- * implement this API via the same endpoint path.
263
+ * Skips only when the S3-compatible backend (e.g. older MinIO) answers
264
+ * that it does not implement PutBucketCors; every other failure throws.
266
265
  */
267
266
  async setCors(bucketName, rules) {
268
267
  try {
@@ -280,27 +279,34 @@ class BucketProvisioner {
280
279
  }));
281
280
  }
282
281
  catch (err) {
283
- // Some S3-compatible backends (e.g. older MinIO) don't support
284
- // PutBucketCors via the standard path. Treat XML parse errors or
285
- // "not implemented" responses as non-fatal.
286
- if (err.Code === 'XmlParseException' ||
287
- err.name === 'XmlParseException' ||
288
- err.Code === 'NotImplemented' ||
289
- err.name === 'NotImplemented' ||
290
- err.message?.includes('not well-formed') ||
291
- err.message?.includes('not implemented') ||
292
- err.message?.includes('CORSConfiguration')) {
282
+ // Some S3-compatible backends (e.g. older MinIO) don't implement
283
+ // PutBucketCors.
284
+ if (isUnsupportedOperation(err)) {
293
285
  return;
294
286
  }
295
287
  throw new types_1.ProvisionerError('CORS_FAILED', `Failed to set CORS on '${bucketName}': ${err.message}`, err);
296
288
  }
297
289
  }
290
+ /**
291
+ * Remove the CORS configuration from an S3 bucket, so no browser origin may
292
+ * reach it. A bucket that has none already is the converged state.
293
+ */
294
+ async clearCors(bucketName) {
295
+ try {
296
+ await this.client.send(new client_s3_1.DeleteBucketCorsCommand({ Bucket: bucketName }));
297
+ }
298
+ catch (err) {
299
+ if (errorCode(err) === 'NoSuchCORSConfiguration' || isUnsupportedOperation(err)) {
300
+ return;
301
+ }
302
+ throw new types_1.ProvisionerError('CORS_FAILED', `Failed to clear CORS on '${bucketName}': ${err.message}`, err);
303
+ }
304
+ }
298
305
  /**
299
306
  * Enable versioning on an S3 bucket.
300
307
  *
301
- * Gracefully skips if the S3-compatible backend (e.g. MinIO edge-cicd)
302
- * does not support PutBucketVersioning — versioning is best-effort
303
- * since not all providers implement this API.
308
+ * Skips only when the S3-compatible backend (e.g. MinIO edge-cicd)
309
+ * answers that it does not implement PutBucketVersioning.
304
310
  */
305
311
  async enableVersioning(bucketName) {
306
312
  try {
@@ -310,16 +316,9 @@ class BucketProvisioner {
310
316
  }));
311
317
  }
312
318
  catch (err) {
313
- // Some S3-compatible backends (e.g. MinIO edge-cicd) don't support
314
- // PutBucketVersioning. Treat XML parse errors or "not implemented"
315
- // responses as non-fatal.
316
- if (err.Code === 'XmlParseException' ||
317
- err.name === 'XmlParseException' ||
318
- err.Code === 'NotImplemented' ||
319
- err.name === 'NotImplemented' ||
320
- err.message?.includes('not well-formed') ||
321
- err.message?.includes('not implemented') ||
322
- err.message?.includes('VersioningConfiguration')) {
319
+ // Some S3-compatible backends (e.g. MinIO edge-cicd) don't implement
320
+ // PutBucketVersioning.
321
+ if (isUnsupportedOperation(err)) {
323
322
  return;
324
323
  }
325
324
  throw new types_1.ProvisionerError('VERSIONING_FAILED', `Failed to enable versioning on '${bucketName}': ${err.message}`, err);
@@ -328,9 +327,8 @@ class BucketProvisioner {
328
327
  /**
329
328
  * Set lifecycle rules on an S3 bucket.
330
329
  *
331
- * Gracefully skips if the S3-compatible backend (e.g. MinIO edge-cicd)
332
- * does not support PutBucketLifecycleConfiguration — lifecycle rules
333
- * are best-effort since not all providers implement this API.
330
+ * Skips only when the S3-compatible backend (e.g. MinIO edge-cicd)
331
+ * answers that it does not implement PutBucketLifecycleConfiguration.
334
332
  */
335
333
  async setLifecycleRules(bucketName, rules) {
336
334
  try {
@@ -347,20 +345,10 @@ class BucketProvisioner {
347
345
  }));
348
346
  }
349
347
  catch (err) {
350
- // Some S3-compatible backends (e.g. MinIO edge-cicd) don't support
351
- // PutBucketLifecycleConfiguration (may require Content-MD5 header
352
- // the SDK doesn't send). Treat XML parse errors, "not implemented",
353
- // or missing-header responses as non-fatal.
354
- if (err.Code === 'XmlParseException' ||
355
- err.name === 'XmlParseException' ||
356
- err.Code === 'NotImplemented' ||
357
- err.name === 'NotImplemented' ||
358
- err.Code === 'MissingContentMD5' ||
359
- err.name === 'MissingContentMD5' ||
360
- err.message?.includes('not well-formed') ||
361
- err.message?.includes('not implemented') ||
362
- err.message?.includes('Content-Md5') ||
363
- err.message?.includes('LifecycleConfiguration')) {
348
+ // Some S3-compatible backends (e.g. MinIO edge-cicd) don't implement
349
+ // PutBucketLifecycleConfiguration, or require a Content-MD5 header the
350
+ // SDK doesn't send.
351
+ if (isUnsupportedOperation(err, ['MissingContentMD5'])) {
364
352
  return;
365
353
  }
366
354
  throw new types_1.ProvisionerError('LIFECYCLE_FAILED', `Failed to set lifecycle rules on '${bucketName}': ${err.message}`, err);