@blamejs/core 0.8.43 → 0.8.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +10 -10
  3. package/index.js +52 -0
  4. package/lib/a2a.js +159 -34
  5. package/lib/acme.js +762 -0
  6. package/lib/ai-pref.js +166 -43
  7. package/lib/api-key.js +108 -47
  8. package/lib/api-snapshot.js +157 -40
  9. package/lib/app-shutdown.js +113 -77
  10. package/lib/archive.js +337 -40
  11. package/lib/arg-parser.js +697 -0
  12. package/lib/asyncapi.js +99 -55
  13. package/lib/atomic-file.js +465 -104
  14. package/lib/audit-chain.js +123 -34
  15. package/lib/audit-daily-review.js +389 -0
  16. package/lib/audit-sign.js +302 -56
  17. package/lib/audit-tools.js +412 -63
  18. package/lib/audit.js +656 -35
  19. package/lib/auth/jwt-external.js +17 -0
  20. package/lib/auth/oauth.js +7 -0
  21. package/lib/auth-bot-challenge.js +505 -0
  22. package/lib/auth-header.js +92 -25
  23. package/lib/backup/bundle.js +26 -0
  24. package/lib/backup/index.js +512 -89
  25. package/lib/backup/manifest.js +168 -7
  26. package/lib/break-glass.js +415 -39
  27. package/lib/budr.js +103 -30
  28. package/lib/bundler.js +86 -66
  29. package/lib/cache.js +192 -72
  30. package/lib/chain-writer.js +65 -40
  31. package/lib/circuit-breaker.js +56 -33
  32. package/lib/cli-helpers.js +106 -75
  33. package/lib/cli.js +6 -30
  34. package/lib/cloud-events.js +99 -32
  35. package/lib/cluster-storage.js +162 -37
  36. package/lib/cluster.js +340 -49
  37. package/lib/codepoint-class.js +66 -0
  38. package/lib/compliance.js +424 -24
  39. package/lib/config-drift.js +111 -46
  40. package/lib/config.js +94 -40
  41. package/lib/consent.js +165 -18
  42. package/lib/constants.js +1 -0
  43. package/lib/content-credentials.js +153 -48
  44. package/lib/cookies.js +154 -62
  45. package/lib/credential-hash.js +133 -61
  46. package/lib/crypto-field.js +702 -18
  47. package/lib/crypto-hpke.js +256 -0
  48. package/lib/crypto.js +744 -22
  49. package/lib/csv.js +178 -35
  50. package/lib/daemon.js +456 -0
  51. package/lib/dark-patterns.js +186 -55
  52. package/lib/db-query.js +79 -2
  53. package/lib/db.js +1431 -60
  54. package/lib/ddl-change-control.js +523 -0
  55. package/lib/deprecate.js +195 -40
  56. package/lib/dev.js +82 -39
  57. package/lib/dora.js +67 -48
  58. package/lib/dr-runbook.js +368 -0
  59. package/lib/dsr.js +142 -11
  60. package/lib/dual-control.js +91 -56
  61. package/lib/events.js +120 -41
  62. package/lib/external-db-migrate.js +192 -2
  63. package/lib/external-db.js +795 -50
  64. package/lib/fapi2.js +122 -1
  65. package/lib/fda-21cfr11.js +395 -0
  66. package/lib/fdx.js +132 -2
  67. package/lib/file-type.js +87 -0
  68. package/lib/file-upload.js +93 -0
  69. package/lib/flag.js +82 -20
  70. package/lib/forms.js +132 -29
  71. package/lib/framework-error.js +169 -0
  72. package/lib/framework-schema.js +163 -35
  73. package/lib/gate-contract.js +849 -175
  74. package/lib/graphql-federation.js +68 -7
  75. package/lib/guard-all.js +172 -55
  76. package/lib/guard-archive.js +286 -124
  77. package/lib/guard-auth.js +194 -21
  78. package/lib/guard-cidr.js +190 -28
  79. package/lib/guard-csv.js +397 -51
  80. package/lib/guard-domain.js +213 -91
  81. package/lib/guard-email.js +236 -29
  82. package/lib/guard-filename.js +307 -75
  83. package/lib/guard-graphql.js +263 -30
  84. package/lib/guard-html.js +310 -116
  85. package/lib/guard-image.js +243 -30
  86. package/lib/guard-json.js +260 -54
  87. package/lib/guard-jsonpath.js +235 -23
  88. package/lib/guard-jwt.js +284 -30
  89. package/lib/guard-markdown.js +204 -22
  90. package/lib/guard-mime.js +190 -26
  91. package/lib/guard-oauth.js +277 -28
  92. package/lib/guard-pdf.js +251 -27
  93. package/lib/guard-regex.js +226 -18
  94. package/lib/guard-shell.js +229 -26
  95. package/lib/guard-svg.js +177 -10
  96. package/lib/guard-template.js +232 -21
  97. package/lib/guard-time.js +195 -29
  98. package/lib/guard-uuid.js +189 -30
  99. package/lib/guard-xml.js +259 -36
  100. package/lib/guard-yaml.js +241 -44
  101. package/lib/honeytoken.js +63 -27
  102. package/lib/html-balance.js +83 -0
  103. package/lib/http-client.js +486 -59
  104. package/lib/http-message-signature.js +582 -0
  105. package/lib/i18n.js +102 -49
  106. package/lib/iab-mspa.js +112 -32
  107. package/lib/iab-tcf.js +107 -2
  108. package/lib/inbox.js +90 -52
  109. package/lib/keychain.js +865 -0
  110. package/lib/legal-hold.js +374 -0
  111. package/lib/local-db-thin.js +320 -0
  112. package/lib/log-stream.js +281 -51
  113. package/lib/log.js +184 -86
  114. package/lib/mail-bounce.js +107 -62
  115. package/lib/mail.js +295 -58
  116. package/lib/mcp.js +108 -27
  117. package/lib/metrics.js +98 -89
  118. package/lib/middleware/age-gate.js +36 -0
  119. package/lib/middleware/ai-act-disclosure.js +37 -0
  120. package/lib/middleware/api-encrypt.js +45 -0
  121. package/lib/middleware/assetlinks.js +40 -0
  122. package/lib/middleware/asyncapi-serve.js +35 -0
  123. package/lib/middleware/attach-user.js +40 -0
  124. package/lib/middleware/bearer-auth.js +40 -0
  125. package/lib/middleware/body-parser.js +230 -0
  126. package/lib/middleware/bot-disclose.js +34 -0
  127. package/lib/middleware/bot-guard.js +39 -0
  128. package/lib/middleware/compression.js +37 -0
  129. package/lib/middleware/cookies.js +32 -0
  130. package/lib/middleware/cors.js +40 -0
  131. package/lib/middleware/csp-nonce.js +40 -0
  132. package/lib/middleware/csp-report.js +34 -0
  133. package/lib/middleware/csrf-protect.js +43 -0
  134. package/lib/middleware/daily-byte-quota.js +53 -85
  135. package/lib/middleware/db-role-for.js +40 -0
  136. package/lib/middleware/dpop.js +40 -0
  137. package/lib/middleware/error-handler.js +37 -14
  138. package/lib/middleware/fetch-metadata.js +39 -0
  139. package/lib/middleware/flag-context.js +34 -0
  140. package/lib/middleware/gpc.js +33 -0
  141. package/lib/middleware/headers.js +35 -0
  142. package/lib/middleware/health.js +46 -0
  143. package/lib/middleware/host-allowlist.js +30 -0
  144. package/lib/middleware/network-allowlist.js +38 -0
  145. package/lib/middleware/openapi-serve.js +34 -0
  146. package/lib/middleware/rate-limit.js +160 -18
  147. package/lib/middleware/request-id.js +36 -18
  148. package/lib/middleware/request-log.js +37 -0
  149. package/lib/middleware/require-aal.js +29 -0
  150. package/lib/middleware/require-auth.js +32 -0
  151. package/lib/middleware/require-bound-key.js +41 -0
  152. package/lib/middleware/require-content-type.js +32 -0
  153. package/lib/middleware/require-methods.js +27 -0
  154. package/lib/middleware/require-mtls.js +33 -0
  155. package/lib/middleware/require-step-up.js +37 -0
  156. package/lib/middleware/security-headers.js +44 -0
  157. package/lib/middleware/security-txt.js +38 -0
  158. package/lib/middleware/span-http-server.js +37 -0
  159. package/lib/middleware/sse.js +36 -0
  160. package/lib/middleware/trace-log-correlation.js +33 -0
  161. package/lib/middleware/trace-propagate.js +32 -0
  162. package/lib/middleware/tus-upload.js +90 -0
  163. package/lib/middleware/web-app-manifest.js +53 -0
  164. package/lib/mtls-ca.js +100 -70
  165. package/lib/network-byte-quota.js +308 -0
  166. package/lib/network-heartbeat.js +135 -0
  167. package/lib/network-tls.js +534 -4
  168. package/lib/network.js +103 -0
  169. package/lib/notify.js +114 -43
  170. package/lib/ntp-check.js +192 -51
  171. package/lib/observability.js +145 -47
  172. package/lib/openapi.js +90 -44
  173. package/lib/outbox.js +99 -1
  174. package/lib/pagination.js +168 -86
  175. package/lib/parsers/index.js +16 -5
  176. package/lib/permissions.js +93 -40
  177. package/lib/pqc-agent.js +84 -8
  178. package/lib/pqc-software.js +94 -60
  179. package/lib/process-spawn.js +95 -21
  180. package/lib/pubsub.js +96 -66
  181. package/lib/queue.js +375 -54
  182. package/lib/redact.js +793 -21
  183. package/lib/render.js +139 -47
  184. package/lib/request-helpers.js +485 -121
  185. package/lib/restore-bundle.js +142 -39
  186. package/lib/restore-rollback.js +136 -45
  187. package/lib/retention.js +178 -50
  188. package/lib/retry.js +116 -33
  189. package/lib/router.js +475 -23
  190. package/lib/safe-async.js +543 -94
  191. package/lib/safe-buffer.js +337 -41
  192. package/lib/safe-json.js +467 -62
  193. package/lib/safe-jsonpath.js +285 -0
  194. package/lib/safe-schema.js +631 -87
  195. package/lib/safe-sql.js +221 -59
  196. package/lib/safe-url.js +278 -46
  197. package/lib/sandbox-worker.js +135 -0
  198. package/lib/sandbox.js +358 -0
  199. package/lib/scheduler.js +135 -70
  200. package/lib/self-update.js +647 -0
  201. package/lib/session-device-binding.js +431 -0
  202. package/lib/session.js +259 -49
  203. package/lib/slug.js +138 -26
  204. package/lib/ssrf-guard.js +316 -56
  205. package/lib/storage.js +433 -70
  206. package/lib/subject.js +405 -23
  207. package/lib/template.js +148 -8
  208. package/lib/tenant-quota.js +545 -0
  209. package/lib/testing.js +440 -53
  210. package/lib/time.js +291 -23
  211. package/lib/tls-exporter.js +239 -0
  212. package/lib/tracing.js +90 -74
  213. package/lib/uuid.js +97 -22
  214. package/lib/vault/index.js +284 -22
  215. package/lib/vault/seal-pem-file.js +66 -0
  216. package/lib/watcher.js +368 -0
  217. package/lib/webhook.js +196 -63
  218. package/lib/websocket.js +393 -68
  219. package/lib/wiki-concepts.js +338 -0
  220. package/lib/worker-pool.js +464 -0
  221. package/package.json +3 -3
  222. package/sbom.cyclonedx.json +7 -7
package/lib/time.js CHANGED
@@ -1,28 +1,34 @@
1
1
  "use strict";
2
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).
3
+ * @module b.time
4
+ * @featured true
5
+ * @nav Tools
6
+ * @title Time
7
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 }
8
+ * @intro
9
+ * Timezone-aware datetime helpers built on top of native
10
+ * `Intl.DateTimeFormat`. No TZ-database vendor; operators get the
11
+ * IANA names Node's ICU build supports (full set on every mainstream
12
+ * platform).
11
13
  *
12
- * b.time.format(d, { timezone, locale, dateStyle, timeStyle })
13
- * operator-readable string
14
+ * The module covers four concerns: parsing ISO 8601 strings into
15
+ * `Date`, decomposing an instant into calendar parts in a named
16
+ * timezone, formatting an instant for human display, and DST-safe
17
+ * calendar arithmetic (addDays / addMonths / startOfDay / endOfDay /
18
+ * diffDays).
14
19
  *
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
+ * Every operation accepts a `Date`, a millisecond-epoch number, or
21
+ * an ISO 8601 string interchangeably. The `timezone` opt defaults
22
+ * to `"UTC"` and the `locale` opt defaults to `"en-US"`.
20
23
  *
21
- * b.time.parseISO(s) → Date | throws TimeError
22
- * b.time.tzOffsetMs(d, timezone) → ms offset (= local - utc)
24
+ * Calendar arithmetic anchors on parts in the requested timezone,
25
+ * not on UTC milliseconds — so `addDays(d, 1, { timezone:
26
+ * "America/New_York" })` always lands on the same wall-clock time
27
+ * the next civil day, even across the spring-forward / fall-back
28
+ * transitions.
23
29
  *
24
- * All ops accept Date, ms-epoch number, or ISO 8601 string. `timezone`
25
- * defaults to UTC. `locale` defaults to "en-US".
30
+ * @card
31
+ * Timezone-aware datetime helpers built on top of native `Intl.DateTimeFormat`.
26
32
  */
27
33
  var C = require("./constants");
28
34
  var { defineClass } = require("./framework-error");
@@ -68,6 +74,35 @@ var WEEKDAY_TO_NUM = {
68
74
  "Mon": 1, "Tue": 2, "Wed": 3, "Thu": 4, "Fri": 5, "Sat": 6, "Sun": 7,
69
75
  };
70
76
 
77
+ /**
78
+ * @primitive b.time.toParts
79
+ * @signature b.time.toParts(input, opts)
80
+ * @since 0.1.0
81
+ * @related b.time.format, b.time.parseISO
82
+ *
83
+ * Decompose an instant into calendar parts as observed in a named
84
+ * timezone. Returns `{ year, month, day, hour, minute, second,
85
+ * millisecond, weekday: 1..7, weekdayName: "Mon".."Sun", dayOfYear }`.
86
+ * Weekday numbering follows ISO 8601 (Monday = 1, Sunday = 7).
87
+ *
88
+ * Accepts a `Date`, ms-epoch number, or ISO 8601 string. `timezone`
89
+ * defaults to `"UTC"`.
90
+ *
91
+ * @opts
92
+ * timezone: string, // IANA name; defaults to "UTC"
93
+ *
94
+ * @example
95
+ * var parts = b.time.toParts("2026-05-09T14:30:00Z", {
96
+ * timezone: "America/New_York",
97
+ * });
98
+ * parts.year; // → 2026
99
+ * parts.month; // → 5
100
+ * parts.day; // → 9
101
+ * parts.hour; // → 10
102
+ * parts.weekdayName; // → "Sat"
103
+ * parts.weekday; // → 6
104
+ * parts.dayOfYear; // → 129
105
+ */
71
106
  function toParts(input, opts) {
72
107
  opts = opts || {};
73
108
  var date = _toDate(input);
@@ -111,6 +146,51 @@ function toParts(input, opts) {
111
146
  return out;
112
147
  }
113
148
 
149
+ /**
150
+ * @primitive b.time.format
151
+ * @signature b.time.format(input, opts)
152
+ * @since 0.1.0
153
+ * @related b.time.toParts, b.time.parseISO
154
+ *
155
+ * Render an instant as an operator-readable string in a named
156
+ * timezone and locale. Accepts the same `Date | number | string`
157
+ * input as the rest of the module. When neither `dateStyle` /
158
+ * `timeStyle` nor any per-field opt is supplied, defaults to
159
+ * `dateStyle: "medium"` + `timeStyle: "short"`.
160
+ *
161
+ * Per-field opts (`year` / `month` / `day` / `hour` / `minute` /
162
+ * `second` / `weekday` / `era` / `hour12` / `fractionalSecondDigits` /
163
+ * `timeZoneName`) pass through to `Intl.DateTimeFormat` unchanged.
164
+ *
165
+ * @opts
166
+ * timezone: string, // IANA name; defaults to "UTC"
167
+ * locale: string, // BCP 47; defaults to "en-US"
168
+ * dateStyle: string, // "full" | "long" | "medium" | "short"
169
+ * timeStyle: string, // "full" | "long" | "medium" | "short"
170
+ * year: string, // "numeric" | "2-digit"
171
+ * month: string, // "numeric" | "2-digit" | "long" | "short" | "narrow"
172
+ * day: string,
173
+ * hour: string,
174
+ * minute: string,
175
+ * second: string,
176
+ * weekday: string,
177
+ * era: string,
178
+ * hour12: boolean,
179
+ * fractionalSecondDigits: number,
180
+ * timeZoneName: string, // "long" | "short" | "shortOffset" | etc.
181
+ *
182
+ * @example
183
+ * var when = "2026-05-09T14:30:00Z";
184
+ * b.time.format(when, { timezone: "America/New_York" });
185
+ * // → "May 9, 2026, 10:30 AM"
186
+ *
187
+ * b.time.format(when, {
188
+ * timezone: "Asia/Tokyo",
189
+ * dateStyle: "full",
190
+ * timeStyle: "long",
191
+ * });
192
+ * // → operator-readable Japanese-locale-style string
193
+ */
114
194
  function format(input, opts) {
115
195
  opts = opts || {};
116
196
  var date = _toDate(input);
@@ -136,6 +216,27 @@ function format(input, opts) {
136
216
  return _dtf(fmtOpts).format(date);
137
217
  }
138
218
 
219
+ /**
220
+ * @primitive b.time.tzOffsetMs
221
+ * @signature b.time.tzOffsetMs(input, timezone)
222
+ * @since 0.1.0
223
+ * @related b.time.toParts, b.time.startOfDay
224
+ *
225
+ * Compute the offset in milliseconds between the named timezone's
226
+ * local wall-clock and UTC at the given instant. Positive east of
227
+ * UTC, negative west. The value depends on the instant — DST
228
+ * transitions are honoured automatically.
229
+ *
230
+ * Throws `TimeError` when `timezone` is missing, non-string, or not
231
+ * an IANA name supported by Node's ICU build.
232
+ *
233
+ * @example
234
+ * var offset = b.time.tzOffsetMs("2026-05-09T12:00:00Z", "America/New_York");
235
+ * // → -14400000 (UTC-4 during DST; 4h * 60m * 60s * 1000ms)
236
+ *
237
+ * var winter = b.time.tzOffsetMs("2026-01-15T12:00:00Z", "America/New_York");
238
+ * // → -18000000 (UTC-5 in standard time)
239
+ */
139
240
  function tzOffsetMs(input, timezone) {
140
241
  var date = _toDate(input);
141
242
  if (!timezone || typeof timezone !== "string") {
@@ -179,6 +280,27 @@ function _fromPartsAtTz(p, timezone) {
179
280
  return new Date(step1 - (offset2 - offset1));
180
281
  }
181
282
 
283
+ /**
284
+ * @primitive b.time.startOfDay
285
+ * @signature b.time.startOfDay(input, opts)
286
+ * @since 0.1.0
287
+ * @related b.time.endOfDay, b.time.diffDays
288
+ *
289
+ * Return a `Date` pointing at midnight (00:00:00.000) of the input's
290
+ * civil day in the named timezone. DST-safe — the spring-forward day
291
+ * still resolves to the first valid wall-clock instant. Useful for
292
+ * day-bucketed audit queries and "is this still today?" comparisons.
293
+ *
294
+ * @opts
295
+ * timezone: string, // IANA name; defaults to "UTC"
296
+ *
297
+ * @example
298
+ * var dayStart = b.time.startOfDay("2026-05-09T14:30:00Z", {
299
+ * timezone: "America/New_York",
300
+ * });
301
+ * dayStart.toISOString();
302
+ * // → "2026-05-09T04:00:00.000Z" (midnight NY = 04:00 UTC during DST)
303
+ */
182
304
  function startOfDay(input, opts) {
183
305
  opts = opts || {};
184
306
  var tz = opts.timezone || DEFAULT_TIMEZONE;
@@ -189,6 +311,27 @@ function startOfDay(input, opts) {
189
311
  }, tz);
190
312
  }
191
313
 
314
+ /**
315
+ * @primitive b.time.endOfDay
316
+ * @signature b.time.endOfDay(input, opts)
317
+ * @since 0.1.0
318
+ * @related b.time.startOfDay, b.time.diffDays
319
+ *
320
+ * Return a `Date` pointing at the last representable millisecond
321
+ * (23:59:59.999) of the input's civil day in the named timezone.
322
+ * DST-safe. Pair with `startOfDay` to bracket "all events on day X
323
+ * in timezone Y" range queries.
324
+ *
325
+ * @opts
326
+ * timezone: string, // IANA name; defaults to "UTC"
327
+ *
328
+ * @example
329
+ * var dayEnd = b.time.endOfDay("2026-05-09T14:30:00Z", {
330
+ * timezone: "America/New_York",
331
+ * });
332
+ * dayEnd.toISOString();
333
+ * // → "2026-05-10T03:59:59.999Z" (23:59:59.999 NY = 03:59 next-day UTC)
334
+ */
192
335
  function endOfDay(input, opts) {
193
336
  opts = opts || {};
194
337
  var tz = opts.timezone || DEFAULT_TIMEZONE;
@@ -199,6 +342,34 @@ function endOfDay(input, opts) {
199
342
  }, tz);
200
343
  }
201
344
 
345
+ /**
346
+ * @primitive b.time.addDays
347
+ * @signature b.time.addDays(input, n, opts)
348
+ * @since 0.1.0
349
+ * @related b.time.addMonths, b.time.diffDays
350
+ *
351
+ * Add `n` calendar days to the input, anchored on the named
352
+ * timezone's wall clock. Negative `n` subtracts. Calendar-day
353
+ * arithmetic — the wall-clock hour / minute / second / millisecond
354
+ * stay the same across DST transitions, even though the resulting
355
+ * UTC offset between the two instants will differ by an hour around
356
+ * the transition.
357
+ *
358
+ * Throws `TimeError` when `n` is not a finite number.
359
+ *
360
+ * @opts
361
+ * timezone: string, // IANA name; defaults to "UTC"
362
+ *
363
+ * @example
364
+ * var due = b.time.addDays("2026-05-09T14:30:00Z", 7, {
365
+ * timezone: "America/New_York",
366
+ * });
367
+ * due.toISOString();
368
+ * // → "2026-05-16T14:30:00.000Z"
369
+ *
370
+ * // Subtract: "yesterday at this time"
371
+ * var yesterday = b.time.addDays(Date.now(), -1, { timezone: "UTC" });
372
+ */
202
373
  function addDays(input, n, opts) {
203
374
  opts = opts || {};
204
375
  if (typeof n !== "number" || !isFinite(n)) {
@@ -216,6 +387,32 @@ function addDays(input, n, opts) {
216
387
  }, tz);
217
388
  }
218
389
 
390
+ /**
391
+ * @primitive b.time.addMonths
392
+ * @signature b.time.addMonths(input, n, opts)
393
+ * @since 0.1.0
394
+ * @related b.time.addDays, b.time.diffDays
395
+ *
396
+ * Add `n` calendar months to the input, anchored on the named
397
+ * timezone's wall clock. Negative `n` subtracts. End-of-month days
398
+ * clamp to the target month's last day — Jan 31 + 1 month is
399
+ * Feb 28/29, not "March 3". Wall-clock hour / minute / second /
400
+ * millisecond are preserved.
401
+ *
402
+ * Throws `TimeError` when `n` is not a finite number.
403
+ *
404
+ * @opts
405
+ * timezone: string, // IANA name; defaults to "UTC"
406
+ *
407
+ * @example
408
+ * var renewal = b.time.addMonths("2026-01-31T09:00:00Z", 1, {
409
+ * timezone: "UTC",
410
+ * });
411
+ * renewal.toISOString();
412
+ * // → "2026-02-28T09:00:00.000Z" (clamped: Feb has no day 31)
413
+ *
414
+ * var nextQuarter = b.time.addMonths(Date.now(), 3, { timezone: "UTC" });
415
+ */
219
416
  function addMonths(input, n, opts) {
220
417
  opts = opts || {};
221
418
  if (typeof n !== "number" || !isFinite(n)) {
@@ -234,6 +431,32 @@ function addMonths(input, n, opts) {
234
431
  }, tz);
235
432
  }
236
433
 
434
+ /**
435
+ * @primitive b.time.diffDays
436
+ * @signature b.time.diffDays(a, b, opts)
437
+ * @since 0.1.0
438
+ * @related b.time.addDays, b.time.startOfDay
439
+ *
440
+ * Calendar days between two instants in the named timezone, computed
441
+ * as `startOfDay(b) - startOfDay(a)` rounded to whole days. Positive
442
+ * when `b` is after `a`; negative otherwise. Foundation for
443
+ * "X days ago" / "Y days until" relative formatting.
444
+ *
445
+ * @opts
446
+ * timezone: string, // IANA name; defaults to "UTC"
447
+ *
448
+ * @example
449
+ * var posted = "2026-05-02T08:00:00Z";
450
+ * var now = "2026-05-09T14:30:00Z";
451
+ * var ago = b.time.diffDays(posted, now, { timezone: "UTC" });
452
+ * // → 7
453
+ *
454
+ * // "X days ago" relative formatting:
455
+ * var label = ago === 0 ? "today"
456
+ * : ago === 1 ? "yesterday"
457
+ * : ago + " days ago";
458
+ * // → "7 days ago"
459
+ */
237
460
  function diffDays(a, b, opts) {
238
461
  opts = opts || {};
239
462
  var tz = opts.timezone || DEFAULT_TIMEZONE;
@@ -244,6 +467,35 @@ function diffDays(a, b, opts) {
244
467
 
245
468
  var ISO_RE = /^(\d{4})-(\d{2})-(\d{2})(?:[T\s](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(Z|[+-]\d{2}:?\d{2})?)?$/;
246
469
 
470
+ /**
471
+ * @primitive b.time.parseISO
472
+ * @signature b.time.parseISO(s)
473
+ * @since 0.1.0
474
+ * @related b.time.toIso8601NoMs, b.time.toParts
475
+ *
476
+ * Parse an ISO 8601 / RFC 3339 datetime string into a `Date`.
477
+ * Accepts `YYYY-MM-DD`, `YYYY-MM-DDTHH:MM`, `YYYY-MM-DDTHH:MM:SS`,
478
+ * optional `.sss` fractional seconds, and an optional trailing
479
+ * `Z` / `+HH:MM` / `-HH:MM` zone designator. A space separator
480
+ * between date and time is also accepted. Strings without a zone
481
+ * designator are interpreted as UTC.
482
+ *
483
+ * Throws `TimeError` for non-strings, malformed input, or
484
+ * out-of-range component values (month > 12, day > 31, hour > 23,
485
+ * etc.).
486
+ *
487
+ * @example
488
+ * var d = b.time.parseISO("2026-05-09T14:30:00Z");
489
+ * d.toISOString(); // → "2026-05-09T14:30:00.000Z"
490
+ *
491
+ * // Offset zone:
492
+ * var withOffset = b.time.parseISO("2026-05-09T10:30:00-04:00");
493
+ * withOffset.toISOString(); // → "2026-05-09T14:30:00.000Z"
494
+ *
495
+ * // Date-only (interpreted as UTC midnight):
496
+ * var date = b.time.parseISO("2026-05-09");
497
+ * date.toISOString(); // → "2026-05-09T00:00:00.000Z"
498
+ */
247
499
  function parseISO(s) {
248
500
  if (typeof s !== "string" || s.length === 0) {
249
501
  throw new TimeError("time/bad-iso", "parseISO: input must be a non-empty string");
@@ -283,12 +535,28 @@ function parseISO(s) {
283
535
  return new Date(utcMs);
284
536
  }
285
537
 
286
- // toIso8601NoMs — emit ISO 8601 with the trailing `.\d{3}Z`
287
- // milliseconds dropped (`2026-05-03T12:34:56Z` instead of
288
- // `…:56.789Z`). Used by SAS / SigV4 / log filename builders that need
289
- // a one-second-resolution timestamp string. Single source of truth so
290
- // the strip pattern lives in one place.
291
538
  var ISO_MS_RE = /\.\d{3}Z$/;
539
+
540
+ /**
541
+ * @primitive b.time.toIso8601NoMs
542
+ * @signature b.time.toIso8601NoMs(input)
543
+ * @since 0.1.0
544
+ * @related b.time.parseISO
545
+ *
546
+ * Emit an ISO 8601 string with the trailing `.sssZ` milliseconds
547
+ * dropped — produces `2026-05-09T14:30:00Z` instead of
548
+ * `2026-05-09T14:30:00.000Z`. Used by SAS / SigV4 / log-filename
549
+ * builders that need a one-second-resolution timestamp string. The
550
+ * strip pattern lives in one place so every caller agrees on the
551
+ * shape.
552
+ *
553
+ * @example
554
+ * b.time.toIso8601NoMs("2026-05-09T14:30:00.789Z");
555
+ * // → "2026-05-09T14:30:00Z"
556
+ *
557
+ * b.time.toIso8601NoMs(new Date(Date.UTC(2026, 4, 9, 14, 30, 0)));
558
+ * // → "2026-05-09T14:30:00Z"
559
+ */
292
560
  function toIso8601NoMs(input) {
293
561
  var d = _toDate(input);
294
562
  return d.toISOString().replace(ISO_MS_RE, "Z");
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.tlsExporter
4
+ * @nav Crypto
5
+ * @title TLS Exporter
6
+ *
7
+ * @intro
8
+ * RFC 5705 / RFC 9266 TLS Exporter for binding application-layer
9
+ * keys and tokens to the live TLS session. The exporter is a
10
+ * deterministic byte-string derived from the TLS 1.3 master secret
11
+ * (RFC 8446 §7.5) — pulling 32 bytes under the
12
+ * `EXPORTER-Channel-Binding` label gives the RFC 9266
13
+ * "tls-exporter" channel-binding identifier.
14
+ *
15
+ * Operators bind bearer tokens, FAPI 2.0 access-token-bound proofs,
16
+ * DPoP `cnf.tbh` claims, mTLS-derived auth headers, and session
17
+ * cookies to the exporter so a captured token cannot be replayed
18
+ * across a different TLS session — even if a downstream proxy
19
+ * re-terminates TLS (RFC 8705). The matching node primitive is
20
+ * `tls.TLSSocket#exportKeyingMaterial(length, label[, context])`.
21
+ *
22
+ * Validation throws at the call site for sockets that aren't TLS
23
+ * (channel binding has no meaning over plaintext), sockets whose
24
+ * protocol is not TLS 1.3 (RFC 9266 §4 conformance), or
25
+ * out-of-range `length` values. Mismatched bindings on
26
+ * `verifyTokenBinding` return `false` rather than throwing —
27
+ * token-binding mismatch is a normal request-time outcome, not a
28
+ * config bug.
29
+ *
30
+ * @card
31
+ * RFC 5705 / RFC 9266 TLS Exporter for binding application-layer keys and tokens to the live TLS session.
32
+ */
33
+
34
+ var crypto = require("./crypto");
35
+ var C = require("./constants");
36
+ var lazyRequire = require("./lazy-require");
37
+ var nb = require("./numeric-bounds");
38
+ var { TlsExporterError } = require("./framework-error");
39
+
40
+ var _err = TlsExporterError.factory;
41
+
42
+ var observability = lazyRequire(function () { return require("./observability"); });
43
+
44
+ var EXPORTER_LABEL = "EXPORTER-Channel-Binding";
45
+ var EXPORTER_LENGTH = C.BYTES.bytes(32);
46
+
47
+ // _resolveTlsSocket — accept either a TLSSocket directly OR an http2/
48
+ // http(s) request whose .socket property is the TLSSocket. Operators
49
+ // almost always pass req.socket; the helper normalizes to the
50
+ // underlying socket so the exportKeyingMaterial call lands on the
51
+ // right object.
52
+ function _resolveTlsSocket(socketOrReq) {
53
+ if (!socketOrReq) {
54
+ throw _err("BAD_INPUT", "tlsExporter: socket or request object required");
55
+ }
56
+ // Express/Node http req → req.socket is the TLSSocket
57
+ var sock = socketOrReq;
58
+ if (typeof socketOrReq.exportKeyingMaterial !== "function" &&
59
+ socketOrReq.socket &&
60
+ typeof socketOrReq.socket.exportKeyingMaterial === "function") {
61
+ sock = socketOrReq.socket;
62
+ }
63
+ if (typeof sock.exportKeyingMaterial !== "function") {
64
+ throw _err("NOT_TLS",
65
+ "tlsExporter: socket has no exportKeyingMaterial — channel binding requires TLS");
66
+ }
67
+ // Per RFC 9266 §4 the exporter is only defined for TLS 1.3. Older
68
+ // protocol versions on the same API would technically return bytes
69
+ // but the channel-binding semantics are NOT RFC 9266 conformant —
70
+ // refuse so an operator-built check doesn't silently fall back to a
71
+ // weaker binding.
72
+ if (typeof sock.getProtocol === "function") {
73
+ var proto = sock.getProtocol();
74
+ if (proto && proto !== "TLSv1.3") {
75
+ throw _err("NOT_TLS_1_3",
76
+ "tlsExporter: TLS protocol is " + proto + ", RFC 9266 requires TLS 1.3");
77
+ }
78
+ }
79
+ return sock;
80
+ }
81
+
82
+ /**
83
+ * @primitive b.tlsExporter.fromSocket
84
+ * @signature b.tlsExporter.fromSocket(socketOrReq, opts)
85
+ * @since 0.7.45
86
+ * @status stable
87
+ * @related b.tlsExporter.bindToken, b.tlsExporter.verifyTokenBinding
88
+ *
89
+ * Extracts a TLS exporter from `socketOrReq` (either a `TLSSocket`
90
+ * directly or an HTTP/HTTP/2 request whose `.socket` is the
91
+ * `TLSSocket`). Defaults match RFC 9266 §4 — 32-byte length, label
92
+ * `EXPORTER-Channel-Binding`, no context — yielding the canonical
93
+ * "tls-exporter" channel-binding identifier. Custom labels and
94
+ * lengths pass through for applications defining their own exporter-
95
+ * derived identifiers; `length` is bounded 16..255 bytes per the
96
+ * keying-material range Node enforces. Throws when the socket is not
97
+ * TLS 1.3 or when the export call fails.
98
+ *
99
+ * @opts
100
+ * {
101
+ * label?: string, // default "EXPORTER-Channel-Binding"
102
+ * length?: number, // default 32; bounded 16..255 bytes
103
+ * context?: Buffer // default null (RFC 8446 §7.5 "no context")
104
+ * }
105
+ *
106
+ * @example
107
+ * var b = require("blamejs").create();
108
+ * var server = b.https.createServer({ key: KEY, cert: CERT }, function (req, res) {
109
+ * var exporter = b.tlsExporter.fromSocket(req, { length: 32 });
110
+ * res.end("exporter bytes: " + exporter.length);
111
+ * // → "exporter bytes: 32"
112
+ * });
113
+ * server.listen(0);
114
+ */
115
+ function fromSocket(socketOrReq, opts) {
116
+ opts = opts || {};
117
+ var label = typeof opts.label === "string" && opts.label.length > 0
118
+ ? opts.label : EXPORTER_LABEL;
119
+ // length is operator-tunable; validate-when-present via numeric-bounds
120
+ // so a non-finite / negative / NaN input surfaces with the same error
121
+ // shape every other framework primitive uses for numeric opts.
122
+ nb.requirePositiveFiniteIntIfPresent(opts.length,
123
+ "tlsExporter.fromSocket: length", TlsExporterError, "BAD_LENGTH");
124
+ var length = opts.length !== undefined ? opts.length : EXPORTER_LENGTH;
125
+ if (length < C.BYTES.bytes(16) || length > C.BYTES.bytes(255)) {
126
+ throw _err("BAD_LENGTH",
127
+ "tlsExporter.fromSocket: length must be 16..255 bytes (got " + length + ")");
128
+ }
129
+ var context = opts.context;
130
+ if (context !== undefined && context !== null && !Buffer.isBuffer(context)) {
131
+ throw _err("BAD_CONTEXT",
132
+ "tlsExporter.fromSocket: context must be Buffer or null");
133
+ }
134
+
135
+ var sock = _resolveTlsSocket(socketOrReq);
136
+ var bytes;
137
+ try {
138
+ // Node's exportKeyingMaterial signature: (length, label, [context]).
139
+ // Passing context=null (the default) corresponds to the RFC 8446
140
+ // §7.5 "no context" case which RFC 9266 §4 mandates for channel
141
+ // binding.
142
+ bytes = context
143
+ ? sock.exportKeyingMaterial(length, label, context)
144
+ : sock.exportKeyingMaterial(length, label);
145
+ } catch (e) {
146
+ throw _err("EXPORT_FAILED",
147
+ "tlsExporter.fromSocket: exportKeyingMaterial threw: " + e.message);
148
+ }
149
+ if (!Buffer.isBuffer(bytes) || bytes.length !== length) {
150
+ throw _err("EXPORT_SHORT",
151
+ "tlsExporter.fromSocket: short exporter (got " + (bytes && bytes.length) + " bytes, want " + length + ")");
152
+ }
153
+
154
+ try { observability().safeEvent("tlsExporter.fromSocket", 1, { outcome: "success" }); }
155
+ catch (_e) { /* drop-silent */ }
156
+ return bytes;
157
+ }
158
+
159
+ /**
160
+ * @primitive b.tlsExporter.bindToken
161
+ * @signature b.tlsExporter.bindToken(socketOrReq, token)
162
+ * @since 0.7.45
163
+ * @status stable
164
+ * @related b.tlsExporter.fromSocket, b.tlsExporter.verifyTokenBinding
165
+ *
166
+ * Binds an opaque token (string or Buffer) to the current TLS session
167
+ * by hashing `SHA3-512(label || exporter || token)`, where `label` is
168
+ * `"blamejs/tls-exporter/bind/v1"`. The framework label keeps the
169
+ * resulting digest distinct from any other place the same exporter +
170
+ * token bytes might be hashed (audit-chain rows, derived-hash columns,
171
+ * etc.) so a binding cannot be reinterpreted across primitives.
172
+ * Operators store the returned hex digest alongside the token and
173
+ * compare via `verifyTokenBinding` on the next request.
174
+ *
175
+ * @example
176
+ * var b = require("blamejs").create();
177
+ * b.https.createServer({ key: KEY, cert: CERT }, function (req, res) {
178
+ * var binding = b.tlsExporter.bindToken(req, "session-token-abc123");
179
+ * binding.length;
180
+ * // → 128 (SHA3-512 hex digest, 64 bytes × 2 hex chars)
181
+ * res.end("ok");
182
+ * }).listen(0);
183
+ */
184
+ function bindToken(socketOrReq, token) {
185
+ if (typeof token !== "string" && !Buffer.isBuffer(token)) {
186
+ throw _err("BAD_TOKEN",
187
+ "tlsExporter.bindToken: token must be a string or Buffer");
188
+ }
189
+ var exporter = fromSocket(socketOrReq);
190
+ var tokenBuf = Buffer.isBuffer(token) ? token : Buffer.from(token, "utf8");
191
+ // SHA3-512 of (label || exporter || token). The label binds the
192
+ // hash to "tls-exporter binding" so the same token + exporter pair
193
+ // does NOT produce the same hash if used in another framework
194
+ // primitive (e.g., the audit-chain row hash).
195
+ var labelBuf = Buffer.from("blamejs/tls-exporter/bind/v1", "utf8");
196
+ return crypto.sha3Hash(Buffer.concat([labelBuf, exporter, tokenBuf]));
197
+ }
198
+
199
+ /**
200
+ * @primitive b.tlsExporter.verifyTokenBinding
201
+ * @signature b.tlsExporter.verifyTokenBinding(socketOrReq, token, claimedBinding)
202
+ * @since 0.7.45
203
+ * @status stable
204
+ * @related b.tlsExporter.fromSocket, b.tlsExporter.bindToken
205
+ *
206
+ * Constant-time compare of a previously-issued `bindToken` digest
207
+ * against a fresh binding computed from the current TLS session.
208
+ * Returns `true` when the digests match (token belongs to this TLS
209
+ * session) and `false` on any mismatch — token-binding mismatch is a
210
+ * normal request-time outcome, so this primitive never throws on
211
+ * mismatch. Throws only when `socketOrReq` is not TLS 1.3 or when
212
+ * the input shape is wrong.
213
+ *
214
+ * @example
215
+ * var b = require("blamejs").create();
216
+ * b.https.createServer({ key: KEY, cert: CERT }, function (req, res) {
217
+ * var stored = b.tlsExporter.bindToken(req, "session-token-abc123");
218
+ * var ok = b.tlsExporter.verifyTokenBinding(req, "session-token-abc123", stored);
219
+ * ok;
220
+ * // → true
221
+ * res.end(ok ? "bound" : "mismatch");
222
+ * }).listen(0);
223
+ */
224
+ function verifyTokenBinding(socketOrReq, token, claimedBinding) {
225
+ var actual = bindToken(socketOrReq, token);
226
+ if (typeof claimedBinding !== "string" || claimedBinding.length === 0) {
227
+ return false;
228
+ }
229
+ return crypto.timingSafeEqual(actual, claimedBinding);
230
+ }
231
+
232
+ module.exports = {
233
+ fromSocket: fromSocket,
234
+ bindToken: bindToken,
235
+ verifyTokenBinding: verifyTokenBinding,
236
+ EXPORTER_LABEL: EXPORTER_LABEL,
237
+ EXPORTER_LENGTH: EXPORTER_LENGTH,
238
+ TlsExporterError: TlsExporterError,
239
+ };