@blamejs/core 0.5.12 → 0.5.14

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.5.x
10
10
 
11
+ - **0.5.13** (2026-04-30) — b.testing.request: supertest-style chainable HTTP test helper
12
+ - **0.5.12** (2026-04-30) — b.middleware.requestLog: HTTP access-log middleware
11
13
  - **0.5.11** (2026-04-30) — b.config: schema-validated environment configuration
12
14
  - **0.5.10** (2026-04-30) — b.middleware.sse: Server-Sent Events
13
15
  - **0.5.9** (2026-04-30) — b.csv: RFC 4180 parser + serializer
package/index.js CHANGED
@@ -108,6 +108,7 @@ var jobs = require("./lib/jobs");
108
108
  var breakGlass = require("./lib/break-glass");
109
109
  var config = require("./lib/config");
110
110
  var csv = require("./lib/csv");
111
+ var time = require("./lib/time");
111
112
  var uuid = require("./lib/uuid");
112
113
  var mail = require("./lib/mail");
113
114
  var mailBounce = require("./lib/mail-bounce");
@@ -213,6 +214,7 @@ module.exports = {
213
214
  breakGlass: breakGlass,
214
215
  config: config,
215
216
  csv: csv,
217
+ time: time,
216
218
  uuid: uuid,
217
219
  mail: mail,
218
220
  mailBounce: mailBounce,
package/lib/testing.js CHANGED
@@ -547,6 +547,165 @@ function listenOnRandomPort(server, host) {
547
547
  // Standalone fake of @opentelemetry/api's minimal subset that
548
548
  // b.tracing actually consumes. No framework primitive owns this.
549
549
 
550
+ // ---- request(target) — supertest-style chainable HTTP test helper ----
551
+ //
552
+ // var res = await b.testing.request(router)
553
+ // .post("/api/widget")
554
+ // .set("X-Request-Id", "abc")
555
+ // .send({ name: "alpha" })
556
+ // .expect(200);
557
+ //
558
+ // res.status, res.headers, res.body (Buffer), res.json (parsed if applicable)
559
+ //
560
+ // Accepts:
561
+ // - a b.router instance (uses .handle(req, res))
562
+ // - a request listener function (req, res) => void
563
+ // - an http.Server / https.Server (used as-is)
564
+ //
565
+ // The framework spins up a real http.Server on an ephemeral port so
566
+ // the request flows through the full Node http stack — same code path
567
+ // production traffic takes. Server is closed automatically when the
568
+ // promise resolves or rejects.
569
+ function request(target) {
570
+ var http = require("node:http");
571
+ // Resolve target → request listener
572
+ var server;
573
+ var ownsServer = false;
574
+ if (target && typeof target.handle === "function") {
575
+ server = http.createServer(function (req, res) {
576
+ Promise.resolve(target.handle(req, res)).catch(function (err) {
577
+ if (!res.headersSent) res.writeHead(500, { "Content-Type": "text/plain" });
578
+ try { res.end((err && err.message) || "Internal Server Error"); } catch (_e) { /* response may already be ended */ }
579
+ });
580
+ });
581
+ ownsServer = true;
582
+ } else if (typeof target === "function") {
583
+ server = http.createServer(target);
584
+ ownsServer = true;
585
+ } else if (target && typeof target.listen === "function" && typeof target.close === "function") {
586
+ server = target;
587
+ ownsServer = false;
588
+ } else {
589
+ throw new Error("b.testing.request: target must be a b.router, a (req,res)=>void function, or an http.Server");
590
+ }
591
+
592
+ function _start(method, path) {
593
+ var headers = {};
594
+ var body = null;
595
+ var expectations = [];
596
+
597
+ var chain = {
598
+ set: function (k, v) {
599
+ if (typeof k === "object" && k !== null) Object.assign(headers, k);
600
+ else headers[k] = v;
601
+ return chain;
602
+ },
603
+ send: function (b) {
604
+ if (b == null) { body = null; return chain; }
605
+ if (Buffer.isBuffer(b) || typeof b === "string") {
606
+ body = b;
607
+ } else {
608
+ body = JSON.stringify(b);
609
+ if (!headers["Content-Type"] && !headers["content-type"]) {
610
+ headers["Content-Type"] = "application/json";
611
+ }
612
+ }
613
+ return chain;
614
+ },
615
+ expect: function (statusOrAssertion) {
616
+ expectations.push(statusOrAssertion);
617
+ return chain;
618
+ },
619
+ then: function (onFulfilled, onRejected) {
620
+ return _execute().then(onFulfilled, onRejected);
621
+ },
622
+ catch: function (onRejected) {
623
+ return _execute().catch(onRejected);
624
+ },
625
+ };
626
+
627
+ function _execute() {
628
+ return new Promise(function (resolve, reject) {
629
+ var listenP;
630
+ if (ownsServer) listenP = listenOnRandomPort(server);
631
+ else listenP = Promise.resolve(server.address() ? server.address().port : null);
632
+
633
+ listenP.then(function (port) {
634
+ var reqOpts = {
635
+ host: "127.0.0.1",
636
+ port: port,
637
+ method: method,
638
+ path: path,
639
+ headers: headers,
640
+ };
641
+ var nodeReq = http.request(reqOpts, function (nodeRes) {
642
+ var chunks = [];
643
+ nodeRes.on("data", function (c) { chunks.push(c); });
644
+ nodeRes.on("end", function () {
645
+ var bodyBuf = Buffer.concat(chunks);
646
+ var bodyText = bodyBuf.toString("utf8");
647
+ var json = null;
648
+ var ct = nodeRes.headers["content-type"] || "";
649
+ if (ct.indexOf("application/json") !== -1) {
650
+ try { json = JSON.parse(bodyText); } catch (_e) { /* leave json null */ }
651
+ }
652
+ var result = {
653
+ status: nodeRes.statusCode,
654
+ headers: nodeRes.headers,
655
+ body: bodyBuf,
656
+ text: bodyText,
657
+ json: json,
658
+ };
659
+ try {
660
+ for (var i = 0; i < expectations.length; i++) {
661
+ var exp = expectations[i];
662
+ if (typeof exp === "number") {
663
+ if (result.status !== exp) {
664
+ throw new Error("expect(" + exp + ") got status " + result.status +
665
+ " body: " + bodyText.slice(0, 200));
666
+ }
667
+ } else if (typeof exp === "function") {
668
+ exp(result);
669
+ } else {
670
+ throw new Error("expect: argument must be a number or function");
671
+ }
672
+ }
673
+ resolve(result);
674
+ } catch (e) {
675
+ reject(e);
676
+ } finally {
677
+ if (ownsServer) try { server.close(); } catch (_e) { /* server may already be closed */ }
678
+ }
679
+ });
680
+ nodeRes.on("error", function (err) {
681
+ if (ownsServer) try { server.close(); } catch (_e) { /* */ }
682
+ reject(err);
683
+ });
684
+ });
685
+ nodeReq.on("error", function (err) {
686
+ if (ownsServer) try { server.close(); } catch (_e) { /* */ }
687
+ reject(err);
688
+ });
689
+ if (body != null) nodeReq.write(body);
690
+ nodeReq.end();
691
+ }, reject);
692
+ });
693
+ }
694
+
695
+ return chain;
696
+ }
697
+
698
+ return {
699
+ get: function (p) { return _start("GET", p); },
700
+ post: function (p) { return _start("POST", p); },
701
+ put: function (p) { return _start("PUT", p); },
702
+ patch: function (p) { return _start("PATCH", p); },
703
+ delete: function (p) { return _start("DELETE", p); },
704
+ head: function (p) { return _start("HEAD", p); },
705
+ options:function (p) { return _start("OPTIONS",p); },
706
+ };
707
+ }
708
+
550
709
  function makeFakeOtelApi() {
551
710
  var spans = [];
552
711
  var activeSpan = null;
@@ -615,6 +774,8 @@ module.exports = {
615
774
  // Async test helpers
616
775
  runMiddleware: runMiddleware,
617
776
  waitFor: waitFor,
777
+ // Chainable HTTP request helper (supertest-style)
778
+ request: request,
618
779
  // Class + constants
619
780
  TestingError: TestingError,
620
781
  DEFAULTS: DEFAULTS,
package/lib/time.js ADDED
@@ -0,0 +1,289 @@
1
+ "use strict";
2
+ /**
3
+ * time — timezone-aware datetime arithmetic + formatting on top of
4
+ * native `Intl.DateTimeFormat`. No TZ-database vendor; operators get
5
+ * the IANA names Node's ICU build supports (full set on every
6
+ * mainstream platform).
7
+ *
8
+ * b.time.toParts(d, { timezone: "America/New_York" })
9
+ * → { year, month, day, hour, minute, second, millisecond,
10
+ * weekday: 1..7, weekdayName: "Mon"..."Sun", dayOfYear }
11
+ *
12
+ * b.time.format(d, { timezone, locale, dateStyle, timeStyle })
13
+ * → operator-readable string
14
+ *
15
+ * b.time.startOfDay(d, { timezone }) → midnight in TZ
16
+ * b.time.endOfDay(d, { timezone }) → 23:59:59.999 in TZ
17
+ * b.time.addDays(d, n, { timezone }) → calendar-day add (DST-safe)
18
+ * b.time.addMonths(d, n, { timezone }) → calendar-month add
19
+ * b.time.diffDays(a, b, { timezone }) → calendar days between
20
+ *
21
+ * b.time.parseISO(s) → Date | throws TimeError
22
+ * b.time.tzOffsetMs(d, timezone) → ms offset (= local - utc)
23
+ *
24
+ * All ops accept Date, ms-epoch number, or ISO 8601 string. `timezone`
25
+ * defaults to UTC. `locale` defaults to "en-US".
26
+ */
27
+ var { defineClass } = require("./framework-error");
28
+
29
+ var TimeError = defineClass("TimeError", { alwaysPermanent: true });
30
+
31
+ var DEFAULT_TIMEZONE = "UTC";
32
+ var DEFAULT_LOCALE = "en-US";
33
+
34
+ var _dtfCache = new Map();
35
+ function _dtf(opts) {
36
+ var key = JSON.stringify(opts);
37
+ if (_dtfCache.has(key)) return _dtfCache.get(key);
38
+ var dtf;
39
+ try { dtf = new Intl.DateTimeFormat(opts.locale || DEFAULT_LOCALE, opts); }
40
+ catch (e) {
41
+ throw new TimeError("time/bad-timezone-or-locale",
42
+ "Intl rejected the timezone/locale: " + ((e && e.message) || String(e)));
43
+ }
44
+ _dtfCache.set(key, dtf);
45
+ return dtf;
46
+ }
47
+
48
+ function _toDate(v) {
49
+ if (v instanceof Date) {
50
+ if (isNaN(v.getTime())) {
51
+ throw new TimeError("time/invalid-date", "input Date is invalid (NaN)");
52
+ }
53
+ return v;
54
+ }
55
+ if (typeof v === "number") {
56
+ if (!isFinite(v)) {
57
+ throw new TimeError("time/invalid-ms", "input must be a finite number of milliseconds");
58
+ }
59
+ return new Date(v);
60
+ }
61
+ if (typeof v === "string") return parseISO(v);
62
+ throw new TimeError("time/bad-input",
63
+ "expected Date | number | ISO string, got " + typeof v);
64
+ }
65
+
66
+ var WEEKDAY_TO_NUM = {
67
+ "Mon": 1, "Tue": 2, "Wed": 3, "Thu": 4, "Fri": 5, "Sat": 6, "Sun": 7,
68
+ };
69
+
70
+ function toParts(input, opts) {
71
+ opts = opts || {};
72
+ var date = _toDate(input);
73
+ var tz = opts.timezone || DEFAULT_TIMEZONE;
74
+ var dtf = _dtf({
75
+ timeZone: tz,
76
+ year: "numeric", month: "2-digit", day: "2-digit",
77
+ hour: "2-digit", minute: "2-digit", second: "2-digit",
78
+ weekday: "short",
79
+ hour12: false,
80
+ });
81
+ var parts = dtf.formatToParts(date);
82
+ var out = { millisecond: date.getUTCMilliseconds() };
83
+ for (var i = 0; i < parts.length; i++) {
84
+ var p = parts[i];
85
+ if (p.type === "year") out.year = parseInt(p.value, 10);
86
+ if (p.type === "month") out.month = parseInt(p.value, 10);
87
+ if (p.type === "day") out.day = parseInt(p.value, 10);
88
+ if (p.type === "hour") out.hour = (p.value === "24" ? 0 : parseInt(p.value, 10));
89
+ if (p.type === "minute") out.minute = parseInt(p.value, 10);
90
+ if (p.type === "second") out.second = parseInt(p.value, 10);
91
+ if (p.type === "weekday") {
92
+ out.weekdayName = p.value;
93
+ out.weekday = WEEKDAY_TO_NUM[p.value] || null;
94
+ }
95
+ }
96
+ // dayOfYear: computed from out.year + out.month + out.day directly,
97
+ // no recursion through toParts. Days-in-month table for non-leap;
98
+ // Feb gets +1 in leap years (Gregorian rule: divisible by 4, not 100,
99
+ // unless 400).
100
+ var DAYS_BEFORE_MONTH = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
101
+ var leap = (out.year % 4 === 0 && out.year % 100 !== 0) || (out.year % 400 === 0);
102
+ out.dayOfYear = DAYS_BEFORE_MONTH[out.month - 1] + out.day + (leap && out.month > 2 ? 1 : 0);
103
+ return out;
104
+ }
105
+
106
+ function format(input, opts) {
107
+ opts = opts || {};
108
+ var date = _toDate(input);
109
+ var fmtOpts = {
110
+ timeZone: opts.timezone || DEFAULT_TIMEZONE,
111
+ locale: opts.locale || DEFAULT_LOCALE,
112
+ };
113
+ if (opts.dateStyle) fmtOpts.dateStyle = opts.dateStyle;
114
+ if (opts.timeStyle) fmtOpts.timeStyle = opts.timeStyle;
115
+ var passthroughKeys = [
116
+ "year", "month", "day", "hour", "minute", "second",
117
+ "weekday", "era", "hour12", "fractionalSecondDigits",
118
+ "timeZoneName",
119
+ ];
120
+ for (var i = 0; i < passthroughKeys.length; i++) {
121
+ var k = passthroughKeys[i];
122
+ if (opts[k] !== undefined) fmtOpts[k] = opts[k];
123
+ }
124
+ if (!opts.dateStyle && !opts.timeStyle && !passthroughKeys.some(function (k) { return opts[k] !== undefined; })) {
125
+ fmtOpts.dateStyle = "medium";
126
+ fmtOpts.timeStyle = "short";
127
+ }
128
+ return _dtf(fmtOpts).format(date);
129
+ }
130
+
131
+ function tzOffsetMs(input, timezone) {
132
+ var date = _toDate(input);
133
+ if (!timezone || typeof timezone !== "string") {
134
+ throw new TimeError("time/bad-timezone",
135
+ "tzOffsetMs: timezone must be a non-empty IANA name");
136
+ }
137
+ var dtf = _dtf({
138
+ timeZone: timezone,
139
+ year: "numeric", month: "2-digit", day: "2-digit",
140
+ hour: "2-digit", minute: "2-digit", second: "2-digit",
141
+ hour12: false,
142
+ });
143
+ var parts = {};
144
+ dtf.formatToParts(date).forEach(function (p) { parts[p.type] = p.value; });
145
+ var hour = parts.hour === "24" ? "00" : parts.hour;
146
+ var asUtcMs = Date.UTC(
147
+ parseInt(parts.year, 10),
148
+ parseInt(parts.month, 10) - 1,
149
+ parseInt(parts.day, 10),
150
+ parseInt(hour, 10),
151
+ parseInt(parts.minute, 10),
152
+ parseInt(parts.second, 10)
153
+ );
154
+ var instantSec = Math.floor(date.getTime() / 1000) * 1000;
155
+ return asUtcMs - instantSec;
156
+ }
157
+
158
+ function _fromPartsAtTz(p, timezone) {
159
+ var candidate = Date.UTC(
160
+ p.year,
161
+ (p.month - 1),
162
+ p.day,
163
+ p.hour || 0,
164
+ p.minute || 0,
165
+ p.second || 0,
166
+ p.millisecond || 0
167
+ );
168
+ var offset1 = tzOffsetMs(candidate, timezone);
169
+ var step1 = candidate - offset1;
170
+ var offset2 = tzOffsetMs(step1, timezone);
171
+ return new Date(step1 - (offset2 - offset1));
172
+ }
173
+
174
+ function startOfDay(input, opts) {
175
+ opts = opts || {};
176
+ var tz = opts.timezone || DEFAULT_TIMEZONE;
177
+ var p = toParts(input, { timezone: tz });
178
+ return _fromPartsAtTz({
179
+ year: p.year, month: p.month, day: p.day,
180
+ hour: 0, minute: 0, second: 0, millisecond: 0,
181
+ }, tz);
182
+ }
183
+
184
+ function endOfDay(input, opts) {
185
+ opts = opts || {};
186
+ var tz = opts.timezone || DEFAULT_TIMEZONE;
187
+ var p = toParts(input, { timezone: tz });
188
+ return _fromPartsAtTz({
189
+ year: p.year, month: p.month, day: p.day,
190
+ hour: 23, minute: 59, second: 59, millisecond: 999,
191
+ }, tz);
192
+ }
193
+
194
+ function addDays(input, n, opts) {
195
+ opts = opts || {};
196
+ if (typeof n !== "number" || !isFinite(n)) {
197
+ throw new TimeError("time/bad-arg", "addDays: n must be a finite number");
198
+ }
199
+ var tz = opts.timezone || DEFAULT_TIMEZONE;
200
+ var p = toParts(input, { timezone: tz });
201
+ var asUtc = new Date(Date.UTC(p.year, p.month - 1, p.day + Math.trunc(n),
202
+ p.hour, p.minute, p.second, p.millisecond));
203
+ return _fromPartsAtTz({
204
+ year: asUtc.getUTCFullYear(),
205
+ month: asUtc.getUTCMonth() + 1,
206
+ day: asUtc.getUTCDate(),
207
+ hour: p.hour, minute: p.minute, second: p.second, millisecond: p.millisecond,
208
+ }, tz);
209
+ }
210
+
211
+ function addMonths(input, n, opts) {
212
+ opts = opts || {};
213
+ if (typeof n !== "number" || !isFinite(n)) {
214
+ throw new TimeError("time/bad-arg", "addMonths: n must be a finite number");
215
+ }
216
+ var tz = opts.timezone || DEFAULT_TIMEZONE;
217
+ var p = toParts(input, { timezone: tz });
218
+ var newMonth0 = (p.month - 1) + Math.trunc(n);
219
+ var newYear = p.year + Math.floor(newMonth0 / 12);
220
+ newMonth0 = ((newMonth0 % 12) + 12) % 12;
221
+ var daysInNew = new Date(Date.UTC(newYear, newMonth0 + 1, 0)).getUTCDate();
222
+ var newDay = Math.min(p.day, daysInNew);
223
+ return _fromPartsAtTz({
224
+ year: newYear, month: newMonth0 + 1, day: newDay,
225
+ hour: p.hour, minute: p.minute, second: p.second, millisecond: p.millisecond,
226
+ }, tz);
227
+ }
228
+
229
+ function diffDays(a, b, opts) {
230
+ opts = opts || {};
231
+ var tz = opts.timezone || DEFAULT_TIMEZONE;
232
+ var aMid = startOfDay(a, { timezone: tz });
233
+ var bMid = startOfDay(b, { timezone: tz });
234
+ return Math.round((bMid.getTime() - aMid.getTime()) / 86400000);
235
+ }
236
+
237
+ var ISO_RE = /^(\d{4})-(\d{2})-(\d{2})(?:[T\s](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(Z|[+-]\d{2}:?\d{2})?)?$/;
238
+
239
+ function parseISO(s) {
240
+ if (typeof s !== "string" || s.length === 0) {
241
+ throw new TimeError("time/bad-iso", "parseISO: input must be a non-empty string");
242
+ }
243
+ var m = ISO_RE.exec(s);
244
+ if (!m) {
245
+ throw new TimeError("time/bad-iso",
246
+ "parseISO: not an ISO 8601 datetime: " + JSON.stringify(s));
247
+ }
248
+ var year = parseInt(m[1], 10);
249
+ var month = parseInt(m[2], 10);
250
+ var day = parseInt(m[3], 10);
251
+ var hour = m[4] ? parseInt(m[4], 10) : 0;
252
+ var minute = m[5] ? parseInt(m[5], 10) : 0;
253
+ var second = m[6] ? parseInt(m[6], 10) : 0;
254
+ var msStr = m[7] || "";
255
+ var ms = msStr ? parseInt((msStr + "000").slice(0, 3), 10) : 0;
256
+ var tz = m[8];
257
+
258
+ if (month < 1 || month > 12 || day < 1 || day > 31 ||
259
+ hour > 23 || minute > 59 || second > 59) {
260
+ throw new TimeError("time/bad-iso",
261
+ "parseISO: out-of-range component in " + JSON.stringify(s));
262
+ }
263
+ var utcMs;
264
+ if (!tz) {
265
+ utcMs = Date.UTC(year, month - 1, day, hour, minute, second, ms);
266
+ } else if (tz === "Z") {
267
+ utcMs = Date.UTC(year, month - 1, day, hour, minute, second, ms);
268
+ } else {
269
+ var sign = tz.charAt(0) === "-" ? -1 : 1;
270
+ var hh = parseInt(tz.slice(1, 3), 10);
271
+ var mm = parseInt(tz.slice(tz.length - 2), 10);
272
+ var offsetMs = sign * (hh * 3600 + mm * 60) * 1000;
273
+ utcMs = Date.UTC(year, month - 1, day, hour, minute, second, ms) - offsetMs;
274
+ }
275
+ return new Date(utcMs);
276
+ }
277
+
278
+ module.exports = {
279
+ toParts: toParts,
280
+ format: format,
281
+ tzOffsetMs: tzOffsetMs,
282
+ startOfDay: startOfDay,
283
+ endOfDay: endOfDay,
284
+ addDays: addDays,
285
+ addMonths: addMonths,
286
+ diffDays: diffDays,
287
+ parseISO: parseISO,
288
+ TimeError: TimeError,
289
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.5.12",
3
+ "version": "0.5.14",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",