@blamejs/core 0.4.24 → 0.4.26

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,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.4.x
10
10
 
11
+ - **0.4.25** (2026-04-30) — b.objectStore.bucketOps: bucket-level operations (SigV4)
12
+ - **0.4.24** (2026-04-30) — b.objectStore: multipart upload + server-side encryption
11
13
  - **0.4.23** (2026-04-30) — b.mail.dkim signing + calendar invites
12
14
  - **0.4.22** (2026-04-30) — b.mail attachments + inline images + plain/HTML alternatives
13
15
  - **0.4.21** (2026-04-30) — b.queue: repeat-in-queue (cron) + parent-child Flows
package/lib/auth/oauth.js CHANGED
@@ -216,15 +216,24 @@ function _validateUrl(url, allowHttp, label) {
216
216
  if (typeof url !== "string" || url.length === 0) {
217
217
  throw new OAuthError("auth-oauth/bad-url", label + ": URL is required");
218
218
  }
219
- var parsed;
220
- try { parsed = new URL(url); }
221
- catch (_e) {
222
- throw new OAuthError("auth-oauth/bad-url", label + ": invalid URL '" + url + "'");
219
+ // Operator-supplied OAuth issuer / endpoint URL — route through
220
+ // safeUrl so the scheme allowlist is consistent with the rest of the
221
+ // framework's outbound gates. Map safe-url's error codes to the
222
+ // domain-specific oauth codes operators already key alerts on.
223
+ try {
224
+ safeUrl.parse(url, {
225
+ allowedProtocols: allowHttp ? safeUrl.ALLOW_HTTP_ALL : safeUrl.ALLOW_HTTP_TLS,
226
+ });
227
+ } catch (e) {
228
+ if (e && e.code === "safe-url/protocol-disallowed") {
229
+ throw new OAuthError("auth-oauth/insecure-url",
230
+ label + ": must be https" + (allowHttp ? " or http" : "") +
231
+ " (got '" + url + "')");
232
+ }
233
+ throw new OAuthError("auth-oauth/bad-url",
234
+ label + ": invalid URL '" + url + "'");
223
235
  }
224
- if (parsed.protocol === "https:") return url;
225
- if (parsed.protocol === "http:" && allowHttp) return url;
226
- throw new OAuthError("auth-oauth/insecure-url",
227
- label + ": must be https (got '" + parsed.protocol + "//" + parsed.host + "')");
236
+ return url;
228
237
  }
229
238
 
230
239
  // ---- JOSE alg → node:crypto verify parameters ----
package/lib/forms.js CHANGED
@@ -36,6 +36,7 @@
36
36
  * forms.escapeHtml = template.escapeHtml (re-export for convenience)
37
37
  */
38
38
  var nodeCrypto = require("crypto");
39
+ var safeUrl = require("./safe-url");
39
40
  var template = require("./template");
40
41
 
41
42
  // ============================================================
@@ -327,8 +328,17 @@ function validate(spec, body) {
327
328
  }
328
329
  }
329
330
  if (f.type === "url" && typeof coerced === "string") {
330
- try { new URL(coerced); }
331
- catch (_e) { errors[f.name] = (f.label || f.name) + " must be a valid URL"; continue; }
331
+ // Form `url` fields come from the request body — operator/external
332
+ // input. Route through safeUrl so the scheme allowlist is honored
333
+ // (https-only by default; operator opts in to http via field meta).
334
+ try {
335
+ safeUrl.parse(coerced, {
336
+ allowedProtocols: f.allowHttp ? safeUrl.ALLOW_HTTP_ALL : safeUrl.ALLOW_HTTP_TLS,
337
+ });
338
+ } catch (_e) {
339
+ errors[f.name] = (f.label || f.name) + " must be a valid URL";
340
+ continue;
341
+ }
332
342
  }
333
343
  if (typeof coerced === "string") {
334
344
  if (f.minlength !== undefined && coerced.length < Number(f.minlength)) {
@@ -45,9 +45,19 @@ var DEFAULT_BLOCKED_AGENTS = [
45
45
  ];
46
46
 
47
47
  var lazyRequire = require("../lazy-require");
48
+ var requestHelpers = require("../request-helpers");
48
49
  var validateOpts = require("../validate-opts");
49
50
  var audit = lazyRequire(function () { return require("../audit"); });
50
51
 
52
+ // Bot-guard's "trust the proxy header" semantics for actor.ip — the
53
+ // audit event records the apparent source even when behind a CDN.
54
+ // extractActorContext defaults to socket.remoteAddress; we override.
55
+ function _xffIp(req) {
56
+ var xff = req.headers && req.headers["x-forwarded-for"];
57
+ if (xff) return String(xff).split(",")[0].trim();
58
+ return (req.socket && req.socket.remoteAddress) || null;
59
+ }
60
+
51
61
  function create(opts) {
52
62
  opts = opts || {};
53
63
  validateOpts(opts, [
@@ -105,10 +115,7 @@ function create(opts) {
105
115
  req.suspectedBot = hit;
106
116
  try {
107
117
  audit().emit({
108
- actor: {
109
- ip: (req.headers && req.headers["x-forwarded-for"]) || (req.socket && req.socket.remoteAddress),
110
- userAgent: req.headers && req.headers["user-agent"],
111
- },
118
+ actor: requestHelpers.extractActorContext(req, { ip: _xffIp(req) }),
112
119
  action: "system.botguard.tag",
113
120
  outcome: "denied",
114
121
  reason: hit,
@@ -121,10 +128,7 @@ function create(opts) {
121
128
 
122
129
  try {
123
130
  audit().emit({
124
- actor: {
125
- ip: (req.headers && req.headers["x-forwarded-for"]) || (req.socket && req.socket.remoteAddress),
126
- userAgent: req.headers && req.headers["user-agent"],
127
- },
131
+ actor: requestHelpers.extractActorContext(req, { ip: _xffIp(req) }),
128
132
  action: "system.botguard.block",
129
133
  outcome: "denied",
130
134
  reason: hit,
@@ -66,6 +66,7 @@
66
66
  */
67
67
  var lazyRequire = require("../lazy-require");
68
68
  var forms = require("../forms");
69
+ var requestHelpers = require("../request-helpers");
69
70
  var validateOpts = require("../validate-opts");
70
71
  var audit = lazyRequire(function () { return require("../audit"); });
71
72
 
@@ -203,7 +204,7 @@ function create(opts) {
203
204
  audit().safeEmit({
204
205
  action: "auth.csrf.denied",
205
206
  outcome: "denied",
206
- actor: { ip: req.socket && req.socket.remoteAddress, userAgent: req.headers && req.headers["user-agent"] },
207
+ actor: requestHelpers.extractActorContext(req),
207
208
  reason: reason,
208
209
  metadata: { method: req.method, path: (req.url || "").split("?")[0] },
209
210
  });
@@ -45,6 +45,7 @@
45
45
  */
46
46
  var C = require("../constants");
47
47
  var lazyRequire = require("../lazy-require");
48
+ var requestHelpers = require("../request-helpers");
48
49
  var validateOpts = require("../validate-opts");
49
50
  var clusterStorage = require("../cluster-storage");
50
51
 
@@ -252,8 +253,11 @@ function create(opts) {
252
253
  if (verdict.retryAfter > 0) res.setHeader("Retry-After", String(verdict.retryAfter));
253
254
  }
254
255
  try {
256
+ // Override `ip` with the x-forwarded-for-aware client IP so the
257
+ // audit event carries the proxied origin even when extractActorContext
258
+ // would have read the socket address.
255
259
  audit().emit({
256
- actor: { ip: _clientIp(req), userAgent: req.headers && req.headers["user-agent"] },
260
+ actor: requestHelpers.extractActorContext(req, { ip: _clientIp(req) }),
257
261
  action: "system.ratelimit.block",
258
262
  outcome: "denied",
259
263
  reason: "rate limit exceeded",
@@ -29,6 +29,7 @@
29
29
  * }
30
30
  */
31
31
  var lazyRequire = require("../lazy-require");
32
+ var requestHelpers = require("../request-helpers");
32
33
  var validateOpts = require("../validate-opts");
33
34
  var audit = lazyRequire(function () { return require("../audit"); });
34
35
 
@@ -61,7 +62,7 @@ function create(opts) {
61
62
  audit().emit({
62
63
  action: "auth.required.denied",
63
64
  outcome: "denied",
64
- actor: { ip: req.socket && req.socket.remoteAddress, userAgent: req.headers && req.headers["user-agent"] },
65
+ actor: requestHelpers.extractActorContext(req),
65
66
  reason: "no authenticated user on request",
66
67
  metadata: { method: req.method, path: req.pathname || (req.url || "").split("?")[0] },
67
68
  });
@@ -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.26",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",