@blamejs/core 0.4.24 → 0.4.25

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/CHANGELOG.md CHANGED
@@ -8,6 +8,7 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.4.x
10
10
 
11
+ - **0.4.24** (2026-04-30) — b.objectStore: multipart upload + server-side encryption
11
12
  - **0.4.23** (2026-04-30) — b.mail.dkim signing + calendar invites
12
13
  - **0.4.22** (2026-04-30) — b.mail attachments + inline images + plain/HTML alternatives
13
14
  - **0.4.21** (2026-04-30) — b.queue: repeat-in-queue (cron) + parent-child Flows
@@ -30,6 +30,7 @@
30
30
  var localProto = require("./local");
31
31
  var httpPutProto = require("./http-put");
32
32
  var sigv4Proto = require("./sigv4");
33
+ var sigv4BucketOps = require("./sigv4-bucket-ops");
33
34
  var gcsProto = require("./gcs");
34
35
  var azureBlobProto = require("./azure-blob");
35
36
  var retryHelper = require("./retry");
@@ -137,4 +138,9 @@ module.exports = {
137
138
  buildBackend: buildBackend,
138
139
  PROTOCOLS: dispatcher.protocols,
139
140
  DEFERRED_PROTOCOLS: dispatcher.deferred,
141
+ // Bucket-level (lifecycle / CORS / create / delete / list) ops are
142
+ // service-scoped, not bucket-scoped — they get their own factory.
143
+ // SigV4 only; GCS / Azure bucket lifecycle differs substantially per
144
+ // cloud and is operator-managed (Terraform / CDK / Pulumi).
145
+ bucketOps: sigv4BucketOps,
140
146
  };
@@ -0,0 +1,491 @@
1
+ "use strict";
2
+ /**
3
+ * sigv4-bucket-ops — bucket-level operations for SigV4 backends.
4
+ *
5
+ * Per-object ops (put / get / list / delete / multipart) live in
6
+ * lib/object-store/sigv4.js and are bound to a single bucket at
7
+ * create() time. Bucket lifecycle ops are at a different level —
8
+ * they need a service-scoped client that addresses arbitrary
9
+ * buckets — so they get their own factory.
10
+ *
11
+ * Operators with multi-cloud ambitions reach for Terraform / CDK /
12
+ * Pulumi. The framework's bucket-ops surface is the operator-from-app
13
+ * path: create the bucket your app needs at boot, attach a lifecycle
14
+ * rule that aborts incomplete multiparts after a week, etc. Niche ops
15
+ * (Object Lock, Replication, Inventory, Notification) are deferred —
16
+ * they're well into Terraform territory.
17
+ *
18
+ * Public API:
19
+ *
20
+ * var ops = b.objectStore.bucketOps.create({
21
+ * protocol: "sigv4",
22
+ * region: "us-east-1",
23
+ * accessKeyId: env("AWS_ACCESS_KEY_ID"),
24
+ * secretAccessKey: env("AWS_SECRET_ACCESS_KEY"),
25
+ * endpoint: "https://s3.us-east-1.amazonaws.com", // optional
26
+ * pathStyle: false,
27
+ * timeoutMs: 30000,
28
+ * });
29
+ *
30
+ * await ops.create("my-bucket", { region: "eu-west-1" });
31
+ * await ops.delete("my-bucket");
32
+ * var buckets = await ops.list(); // [{ name, creationDate }]
33
+ * await ops.setLifecycle("my-bucket", [{
34
+ * id: "abort-stale-multiparts",
35
+ * status: "Enabled",
36
+ * prefix: "",
37
+ * abortIncompleteMultipartUpload: { daysAfterInitiation: 7 },
38
+ * }]);
39
+ * await ops.setCorsRules("my-bucket", [{
40
+ * allowedOrigins: ["https://app.example.com"],
41
+ * allowedMethods: ["GET", "PUT", "POST"],
42
+ * allowedHeaders: ["*"],
43
+ * exposeHeaders: ["ETag"],
44
+ * maxAgeSeconds: 3600,
45
+ * }]);
46
+ *
47
+ * Validation is Tier-A: every input shape is rejected at the call
48
+ * site rather than producing a server-side 400. Errors surface as
49
+ * ObjectStoreError with codes (BUCKET_INVALID_NAME, INVALID_LIFECYCLE,
50
+ * INVALID_CORS_RULE, BUCKET_ALREADY_OWNED, BUCKET_NOT_EMPTY, etc.).
51
+ */
52
+ var { URL } = require("url");
53
+ var nodeCrypto = require("crypto");
54
+ var sigv4 = require("./sigv4");
55
+ var safeXml = require("../parsers/safe-xml");
56
+ var safeUrl = require("../safe-url");
57
+ var httpClient = require("../http-client");
58
+ var { ObjectStoreError } = require("../framework-error");
59
+
60
+ var _err = ObjectStoreError.factory;
61
+
62
+ // S3 bucket-name rules (general purpose). Source: AWS docs
63
+ // "Bucket naming rules". Lowercase letters, digits, hyphens; 3..63
64
+ // chars; no consecutive dots; cannot end in -s3alias / -ol-s3 etc.
65
+ // We catch the common-mistake cases at config time; AWS catches the
66
+ // rest at request time.
67
+ var BUCKET_NAME_RE = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
68
+
69
+ function _validateBucketName(name) {
70
+ if (typeof name !== "string" || name.length < 3 || name.length > 63) {
71
+ throw _err("BUCKET_INVALID_NAME",
72
+ "bucket name must be a string of length 3..63, got " +
73
+ (typeof name === "string" ? "length " + name.length : typeof name), true);
74
+ }
75
+ if (!BUCKET_NAME_RE.test(name)) {
76
+ throw _err("BUCKET_INVALID_NAME",
77
+ "bucket name '" + name + "' violates S3 naming rules " +
78
+ "(lowercase, digits, hyphens, dots — no leading/trailing punct)", true);
79
+ }
80
+ if (name.indexOf("..") !== -1) {
81
+ throw _err("BUCKET_INVALID_NAME",
82
+ "bucket name '" + name + "' contains consecutive dots", true);
83
+ }
84
+ }
85
+
86
+ function _xmlEscape(s) {
87
+ return String(s)
88
+ .replace(/&/g, "&amp;")
89
+ .replace(/</g, "&lt;")
90
+ .replace(/>/g, "&gt;")
91
+ .replace(/"/g, "&quot;")
92
+ .replace(/'/g, "&apos;");
93
+ }
94
+
95
+ function _md5Base64(buf) {
96
+ return nodeCrypto.createHash("md5").update(buf).digest("base64");
97
+ }
98
+
99
+ // ---- Lifecycle XML ----
100
+
101
+ var ALLOWED_STORAGE_CLASSES = [
102
+ "STANDARD", "REDUCED_REDUNDANCY", "STANDARD_IA", "ONEZONE_IA",
103
+ "INTELLIGENT_TIERING", "GLACIER", "DEEP_ARCHIVE", "GLACIER_IR",
104
+ "EXPRESS_ONEZONE",
105
+ ];
106
+
107
+ function _buildLifecycleXml(rules) {
108
+ if (!Array.isArray(rules) || rules.length === 0) {
109
+ throw _err("INVALID_LIFECYCLE",
110
+ "setLifecycle: rules must be a non-empty array", true);
111
+ }
112
+ if (rules.length > 1000) {
113
+ throw _err("INVALID_LIFECYCLE",
114
+ "setLifecycle: maximum 1000 rules per bucket (S3 spec)", true);
115
+ }
116
+ var body = '<?xml version="1.0" encoding="UTF-8"?>';
117
+ body += '<LifecycleConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">';
118
+ for (var i = 0; i < rules.length; i++) {
119
+ var rule = rules[i];
120
+ if (!rule || typeof rule !== "object") {
121
+ throw _err("INVALID_LIFECYCLE",
122
+ "rules[" + i + "] must be an object", true);
123
+ }
124
+ var status = rule.status || "Enabled";
125
+ if (status !== "Enabled" && status !== "Disabled") {
126
+ throw _err("INVALID_LIFECYCLE",
127
+ "rules[" + i + "].status must be 'Enabled' or 'Disabled'", true);
128
+ }
129
+ if (!rule.expiration && !rule.transition && !rule.abortIncompleteMultipartUpload) {
130
+ throw _err("INVALID_LIFECYCLE",
131
+ "rules[" + i + "] must specify at least one of " +
132
+ "expiration / transition / abortIncompleteMultipartUpload", true);
133
+ }
134
+ body += "<Rule>";
135
+ if (rule.id !== undefined) {
136
+ if (typeof rule.id !== "string" || rule.id.length === 0) {
137
+ throw _err("INVALID_LIFECYCLE",
138
+ "rules[" + i + "].id must be a non-empty string when set", true);
139
+ }
140
+ body += "<ID>" + _xmlEscape(rule.id) + "</ID>";
141
+ }
142
+ body += "<Filter><Prefix>" + _xmlEscape(rule.prefix || "") + "</Prefix></Filter>";
143
+ body += "<Status>" + status + "</Status>";
144
+ if (rule.expiration) {
145
+ body += "<Expiration>";
146
+ if (rule.expiration.days !== undefined) {
147
+ if (typeof rule.expiration.days !== "number" || rule.expiration.days < 1) {
148
+ throw _err("INVALID_LIFECYCLE",
149
+ "rules[" + i + "].expiration.days must be a positive integer", true);
150
+ }
151
+ body += "<Days>" + rule.expiration.days + "</Days>";
152
+ }
153
+ if (rule.expiration.date !== undefined) {
154
+ body += "<Date>" + _xmlEscape(rule.expiration.date) + "</Date>";
155
+ }
156
+ if (rule.expiration.expiredObjectDeleteMarker !== undefined) {
157
+ body += "<ExpiredObjectDeleteMarker>" +
158
+ (rule.expiration.expiredObjectDeleteMarker ? "true" : "false") +
159
+ "</ExpiredObjectDeleteMarker>";
160
+ }
161
+ body += "</Expiration>";
162
+ }
163
+ if (rule.transition) {
164
+ if (ALLOWED_STORAGE_CLASSES.indexOf(rule.transition.storageClass) === -1) {
165
+ throw _err("INVALID_LIFECYCLE",
166
+ "rules[" + i + "].transition.storageClass must be one of: " +
167
+ ALLOWED_STORAGE_CLASSES.join(", "), true);
168
+ }
169
+ body += "<Transition>";
170
+ if (rule.transition.days !== undefined) {
171
+ body += "<Days>" + rule.transition.days + "</Days>";
172
+ }
173
+ if (rule.transition.date !== undefined) {
174
+ body += "<Date>" + _xmlEscape(rule.transition.date) + "</Date>";
175
+ }
176
+ body += "<StorageClass>" + rule.transition.storageClass + "</StorageClass>";
177
+ body += "</Transition>";
178
+ }
179
+ if (rule.abortIncompleteMultipartUpload) {
180
+ var dai = rule.abortIncompleteMultipartUpload.daysAfterInitiation;
181
+ if (typeof dai !== "number" || dai < 1) {
182
+ throw _err("INVALID_LIFECYCLE",
183
+ "rules[" + i + "].abortIncompleteMultipartUpload.daysAfterInitiation " +
184
+ "must be a positive integer", true);
185
+ }
186
+ body += "<AbortIncompleteMultipartUpload>";
187
+ body += "<DaysAfterInitiation>" + dai + "</DaysAfterInitiation>";
188
+ body += "</AbortIncompleteMultipartUpload>";
189
+ }
190
+ body += "</Rule>";
191
+ }
192
+ body += "</LifecycleConfiguration>";
193
+ return body;
194
+ }
195
+
196
+ // ---- CORS XML ----
197
+
198
+ var ALLOWED_CORS_METHODS = ["GET", "PUT", "POST", "DELETE", "HEAD"];
199
+
200
+ function _buildCorsXml(rules) {
201
+ if (!Array.isArray(rules) || rules.length === 0) {
202
+ throw _err("INVALID_CORS_RULE",
203
+ "setCorsRules: rules must be a non-empty array", true);
204
+ }
205
+ if (rules.length > 100) {
206
+ throw _err("INVALID_CORS_RULE",
207
+ "setCorsRules: maximum 100 rules per bucket (S3 spec)", true);
208
+ }
209
+ var body = '<?xml version="1.0" encoding="UTF-8"?>';
210
+ body += '<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">';
211
+ for (var i = 0; i < rules.length; i++) {
212
+ var rule = rules[i];
213
+ if (!rule || typeof rule !== "object") {
214
+ throw _err("INVALID_CORS_RULE",
215
+ "rules[" + i + "] must be an object", true);
216
+ }
217
+ if (!Array.isArray(rule.allowedOrigins) || rule.allowedOrigins.length === 0) {
218
+ throw _err("INVALID_CORS_RULE",
219
+ "rules[" + i + "].allowedOrigins must be a non-empty array", true);
220
+ }
221
+ if (!Array.isArray(rule.allowedMethods) || rule.allowedMethods.length === 0) {
222
+ throw _err("INVALID_CORS_RULE",
223
+ "rules[" + i + "].allowedMethods must be a non-empty array", true);
224
+ }
225
+ for (var m = 0; m < rule.allowedMethods.length; m++) {
226
+ if (ALLOWED_CORS_METHODS.indexOf(rule.allowedMethods[m]) === -1) {
227
+ throw _err("INVALID_CORS_RULE",
228
+ "rules[" + i + "].allowedMethods[" + m + "] must be one of: " +
229
+ ALLOWED_CORS_METHODS.join(", "), true);
230
+ }
231
+ }
232
+ body += "<CORSRule>";
233
+ if (rule.id !== undefined) body += "<ID>" + _xmlEscape(rule.id) + "</ID>";
234
+ rule.allowedOrigins.forEach(function (o) {
235
+ body += "<AllowedOrigin>" + _xmlEscape(o) + "</AllowedOrigin>";
236
+ });
237
+ rule.allowedMethods.forEach(function (m) {
238
+ body += "<AllowedMethod>" + _xmlEscape(m) + "</AllowedMethod>";
239
+ });
240
+ if (Array.isArray(rule.allowedHeaders)) {
241
+ rule.allowedHeaders.forEach(function (h) {
242
+ body += "<AllowedHeader>" + _xmlEscape(h) + "</AllowedHeader>";
243
+ });
244
+ }
245
+ if (Array.isArray(rule.exposeHeaders)) {
246
+ rule.exposeHeaders.forEach(function (h) {
247
+ body += "<ExposeHeader>" + _xmlEscape(h) + "</ExposeHeader>";
248
+ });
249
+ }
250
+ if (rule.maxAgeSeconds !== undefined) {
251
+ if (typeof rule.maxAgeSeconds !== "number" || rule.maxAgeSeconds < 0) {
252
+ throw _err("INVALID_CORS_RULE",
253
+ "rules[" + i + "].maxAgeSeconds must be a non-negative number", true);
254
+ }
255
+ body += "<MaxAgeSeconds>" + rule.maxAgeSeconds + "</MaxAgeSeconds>";
256
+ }
257
+ body += "</CORSRule>";
258
+ }
259
+ body += "</CORSConfiguration>";
260
+ if (Buffer.byteLength(body, "utf8") > 64 * 1024) {
261
+ throw _err("INVALID_CORS_RULE",
262
+ "CORS configuration exceeds 64 KB (S3 spec)", true);
263
+ }
264
+ return body;
265
+ }
266
+
267
+ // ---- Public factory ----
268
+
269
+ function create(config) {
270
+ if (!config || typeof config !== "object") {
271
+ throw _err("INVALID_CONFIG", "bucketOps.create requires a config object", true);
272
+ }
273
+ if (config.protocol && config.protocol !== "sigv4") {
274
+ throw _err("INVALID_CONFIG",
275
+ "bucketOps currently only supports protocol 'sigv4'; got '" +
276
+ config.protocol + "'. GCS and Azure bucket lifecycle differs " +
277
+ "substantially per cloud and is operator-managed (Terraform / " +
278
+ "CDK / Pulumi).", true);
279
+ }
280
+ if (!config.region) throw _err("INVALID_CONFIG", "bucketOps: region is required", true);
281
+ if (!config.accessKeyId) throw _err("INVALID_CONFIG", "bucketOps: accessKeyId is required", true);
282
+ if (!config.secretAccessKey) throw _err("INVALID_CONFIG", "bucketOps: secretAccessKey is required", true);
283
+
284
+ var endpoint = config.endpoint || ("https://s3." + config.region + ".amazonaws.com");
285
+ if (endpoint.endsWith("/")) endpoint = endpoint.slice(0, -1);
286
+ var pathStyle = !!(config.pathStyle || config.forcePathStyle);
287
+ var allowedProtocols = config.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
288
+ var allowInternal = config.allowInternal != null ? config.allowInternal : null;
289
+ safeUrl.parse(endpoint, {
290
+ allowedProtocols: allowedProtocols,
291
+ errorClass: ObjectStoreError,
292
+ });
293
+ var reqOpts = { timeoutMs: config.timeoutMs, allowedProtocols: allowedProtocols };
294
+ if (allowInternal !== null) reqOpts.allowInternal = allowInternal;
295
+
296
+ function _bucketUrl(name, query) {
297
+ var u;
298
+ if (pathStyle) {
299
+ u = new URL(endpoint + "/" + name + "/");
300
+ } else {
301
+ u = new URL(endpoint);
302
+ u.hostname = name + "." + u.hostname;
303
+ u.pathname = "/";
304
+ }
305
+ if (query) {
306
+ Object.keys(query).forEach(function (k) {
307
+ u.searchParams.set(k, query[k] != null ? query[k] : "");
308
+ });
309
+ }
310
+ return u;
311
+ }
312
+
313
+ function _serviceUrl(query) {
314
+ // ListBuckets — service-level, no bucket prefix.
315
+ var u = new URL(endpoint);
316
+ u.pathname = "/";
317
+ if (query) {
318
+ Object.keys(query).forEach(function (k) { u.searchParams.set(k, query[k]); });
319
+ }
320
+ return u;
321
+ }
322
+
323
+ function _signed(method, url, payloadHash, extraHeaders) {
324
+ var signed = sigv4.signRequest({
325
+ method: method,
326
+ url: url,
327
+ headers: extraHeaders || {},
328
+ payloadHash: payloadHash,
329
+ region: config.region,
330
+ accessKeyId: config.accessKeyId,
331
+ secretAccessKey: config.secretAccessKey,
332
+ sessionToken: config.sessionToken,
333
+ });
334
+ return signed.headers;
335
+ }
336
+
337
+ function _request(method, url, headers, body) {
338
+ return httpClient.request({
339
+ method: method,
340
+ url: url,
341
+ headers: headers,
342
+ body: body,
343
+ idleTimeoutMs: reqOpts.timeoutMs,
344
+ errorClass: ObjectStoreError,
345
+ allowedProtocols: reqOpts.allowedProtocols,
346
+ ...((reqOpts.allowInternal !== undefined) ? { allowInternal: reqOpts.allowInternal } : {}),
347
+ });
348
+ }
349
+
350
+ // ---- create ----
351
+
352
+ function createBucket(name, opts) {
353
+ _validateBucketName(name);
354
+ opts = opts || {};
355
+ var targetRegion = opts.region || config.region;
356
+ var url = _bucketUrl(name);
357
+ var bodyBuf;
358
+ var extra = {};
359
+ // us-east-1 takes an empty body. Other regions need
360
+ // CreateBucketConfiguration with a LocationConstraint.
361
+ if (targetRegion && targetRegion !== "us-east-1") {
362
+ bodyBuf = Buffer.from(
363
+ '<?xml version="1.0" encoding="UTF-8"?>' +
364
+ '<CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">' +
365
+ '<LocationConstraint>' + _xmlEscape(targetRegion) + '</LocationConstraint>' +
366
+ '</CreateBucketConfiguration>',
367
+ "utf8"
368
+ );
369
+ extra["Content-Type"] = "application/xml";
370
+ extra["Content-Length"] = String(bodyBuf.length);
371
+ } else {
372
+ bodyBuf = Buffer.alloc(0);
373
+ extra["Content-Length"] = "0";
374
+ }
375
+ var payloadHash = sigv4.sha256Hex(bodyBuf);
376
+ var headers = _signed("PUT", url, payloadHash, extra);
377
+ return _request("PUT", url, headers, bodyBuf).then(
378
+ function () { return { created: true, name: name, region: targetRegion }; },
379
+ function (e) {
380
+ // Map S3 conflict response codes into stable framework codes.
381
+ if (e.statusCode === 409 && /BucketAlreadyOwnedByYou/.test(e.message || "")) {
382
+ throw _err("BUCKET_ALREADY_OWNED",
383
+ "bucket '" + name + "' already exists and is owned by this account", true);
384
+ }
385
+ if (e.statusCode === 409) {
386
+ throw _err("BUCKET_NAME_TAKEN",
387
+ "bucket name '" + name + "' is taken in S3's global namespace", true);
388
+ }
389
+ throw e;
390
+ }
391
+ );
392
+ }
393
+
394
+ // ---- delete ----
395
+
396
+ function deleteBucket(name) {
397
+ _validateBucketName(name);
398
+ var url = _bucketUrl(name);
399
+ var payloadHash = sigv4.sha256Hex(Buffer.alloc(0));
400
+ var headers = _signed("DELETE", url, payloadHash);
401
+ return _request("DELETE", url, headers, null).then(
402
+ function () { return true; },
403
+ function (e) {
404
+ if (e.statusCode === 404) return false;
405
+ if (e.statusCode === 409 && /BucketNotEmpty/.test(e.message || "")) {
406
+ throw _err("BUCKET_NOT_EMPTY",
407
+ "bucket '" + name + "' is not empty; delete all objects + " +
408
+ "noncurrent versions + delete-markers first", true);
409
+ }
410
+ throw e;
411
+ }
412
+ );
413
+ }
414
+
415
+ // ---- list ----
416
+
417
+ function listBuckets() {
418
+ var url = _serviceUrl();
419
+ var payloadHash = sigv4.sha256Hex(Buffer.alloc(0));
420
+ var headers = _signed("GET", url, payloadHash);
421
+ return _request("GET", url, headers, null).then(function (res) {
422
+ var doc = safeXml.parse(res.body);
423
+ var result = doc.ListAllMyBucketsResult || {};
424
+ var bucketsContainer = result.Buckets || {};
425
+ var raw = bucketsContainer.Bucket;
426
+ if (!raw) return [];
427
+ var arr = Array.isArray(raw) ? raw : [raw];
428
+ return arr.map(function (b) {
429
+ return {
430
+ name: b.Name,
431
+ creationDate: b.CreationDate ? Date.parse(b.CreationDate) : null,
432
+ region: b.BucketRegion || null,
433
+ };
434
+ });
435
+ });
436
+ }
437
+
438
+ // ---- setLifecycle ----
439
+
440
+ function setLifecycle(name, rules) {
441
+ _validateBucketName(name);
442
+ var bodyXml = _buildLifecycleXml(rules);
443
+ var bodyBuf = Buffer.from(bodyXml, "utf8");
444
+ var url = _bucketUrl(name, { lifecycle: "" });
445
+ var payloadHash = sigv4.sha256Hex(bodyBuf);
446
+ var headers = _signed("PUT", url, payloadHash, {
447
+ "Content-Type": "application/xml",
448
+ "Content-Length": String(bodyBuf.length),
449
+ "Content-MD5": _md5Base64(bodyBuf),
450
+ });
451
+ return _request("PUT", url, headers, bodyBuf).then(function () {
452
+ return { applied: true, name: name, ruleCount: rules.length };
453
+ });
454
+ }
455
+
456
+ // ---- setCorsRules ----
457
+
458
+ function setCorsRules(name, rules) {
459
+ _validateBucketName(name);
460
+ var bodyXml = _buildCorsXml(rules);
461
+ var bodyBuf = Buffer.from(bodyXml, "utf8");
462
+ var url = _bucketUrl(name, { cors: "" });
463
+ var payloadHash = sigv4.sha256Hex(bodyBuf);
464
+ var headers = _signed("PUT", url, payloadHash, {
465
+ "Content-Type": "application/xml",
466
+ "Content-Length": String(bodyBuf.length),
467
+ "Content-MD5": _md5Base64(bodyBuf),
468
+ });
469
+ return _request("PUT", url, headers, bodyBuf).then(function () {
470
+ return { applied: true, name: name, ruleCount: rules.length };
471
+ });
472
+ }
473
+
474
+ return {
475
+ protocol: "sigv4",
476
+ create: createBucket,
477
+ delete: deleteBucket,
478
+ list: listBuckets,
479
+ setLifecycle: setLifecycle,
480
+ setCorsRules: setCorsRules,
481
+ };
482
+ }
483
+
484
+ module.exports = {
485
+ create: create,
486
+ // Test-only exports for unit-testing the XML builders without
487
+ // standing up a fake S3 server.
488
+ _buildLifecycleXmlForTest: _buildLifecycleXml,
489
+ _buildCorsXmlForTest: _buildCorsXml,
490
+ _validateBucketNameForTest: _validateBucketName,
491
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.4.24",
3
+ "version": "0.4.25",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",