@blamejs/blamejs-shop 0.5.19 → 0.5.21

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 (194) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +1 -0
  3. package/SECURITY.md +17 -0
  4. package/lib/addresses.js +2 -2
  5. package/lib/admin.js +49 -45
  6. package/lib/affiliates.js +7 -23
  7. package/lib/announcement-bar.js +5 -22
  8. package/lib/api-keys.js +4 -13
  9. package/lib/asset-manifest.json +1 -1
  10. package/lib/banner-ab-tests.js +5 -9
  11. package/lib/blog-articles.js +8 -26
  12. package/lib/captcha-gate.js +7 -17
  13. package/lib/carrier-accounts.js +3 -10
  14. package/lib/catalog-drafts.js +5 -12
  15. package/lib/category-navigation.js +4 -20
  16. package/lib/cms-blocks.js +4 -7
  17. package/lib/compliance-export.js +4 -11
  18. package/lib/consent-ledger.js +2 -4
  19. package/lib/cost-layers.js +56 -21
  20. package/lib/customer-import.js +2 -2
  21. package/lib/customer-notes.js +7 -23
  22. package/lib/customer-roles.js +3 -5
  23. package/lib/customer-segments.js +17 -14
  24. package/lib/customer-surveys.js +7 -14
  25. package/lib/customers.js +2 -4
  26. package/lib/dispute-resolution.js +5 -13
  27. package/lib/dropship-forwarding.js +3 -5
  28. package/lib/dunning.js +2 -7
  29. package/lib/email-ab-tests.js +4 -8
  30. package/lib/email-campaigns.js +25 -14
  31. package/lib/email-warmup.js +2 -7
  32. package/lib/experiments.js +6 -10
  33. package/lib/externaldb-d1.js +78 -44
  34. package/lib/geolocation.js +18 -28
  35. package/lib/gift-options.js +5 -23
  36. package/lib/knowledge-base.js +7 -22
  37. package/lib/line-gift-wrap.js +3 -17
  38. package/lib/live-chat.js +5 -18
  39. package/lib/metered-usage.js +3 -8
  40. package/lib/notifications.js +3 -7
  41. package/lib/operator-accounts.js +3 -5
  42. package/lib/operator-approvals.js +6 -8
  43. package/lib/operator-audit-log.js +2 -7
  44. package/lib/operator-help-center.js +5 -21
  45. package/lib/operator-inbox.js +4 -11
  46. package/lib/operator-roles.js +5 -7
  47. package/lib/order-escalation.js +2 -2
  48. package/lib/order-export.js +24 -20
  49. package/lib/order-notes.js +4 -14
  50. package/lib/order-ratings.js +4 -10
  51. package/lib/payment-methods.js +5 -13
  52. package/lib/payment-retries.js +2 -7
  53. package/lib/payment.js +103 -58
  54. package/lib/pixel-events.js +8 -23
  55. package/lib/plan-changes.js +2 -7
  56. package/lib/product-qa.js +4 -8
  57. package/lib/promo-banners.js +7 -15
  58. package/lib/purchase-orders.js +3 -6
  59. package/lib/push-notifications.js +7 -32
  60. package/lib/pwa-manifest.js +6 -11
  61. package/lib/quotes.js +5 -9
  62. package/lib/referrals.js +10 -6
  63. package/lib/reviews.js +4 -10
  64. package/lib/robots-config.js +6 -14
  65. package/lib/sales-tax-filings.js +2 -2
  66. package/lib/search-facets.js +5 -5
  67. package/lib/search-ranking.js +4 -7
  68. package/lib/search-synonyms.js +7 -21
  69. package/lib/security-middleware.js +125 -70
  70. package/lib/seller-signup.js +10 -26
  71. package/lib/sidebar-widgets.js +6 -15
  72. package/lib/site-redirects.js +49 -137
  73. package/lib/sitemap-generator.js +3 -7
  74. package/lib/sms-dispatcher.js +7 -23
  75. package/lib/stock-receipts.js +4 -10
  76. package/lib/storefront-dashboards.js +2 -10
  77. package/lib/storefront-forms.js +9 -22
  78. package/lib/storefront-pages.js +6 -20
  79. package/lib/storefront.js +21 -23
  80. package/lib/subscription-billing.js +3 -8
  81. package/lib/subscription-controls.js +2 -7
  82. package/lib/suggestion-box.js +9 -26
  83. package/lib/support-tickets.js +6 -24
  84. package/lib/tax-remittance.js +2 -2
  85. package/lib/tenants.js +2 -7
  86. package/lib/text-guard.js +137 -35
  87. package/lib/theme-assets.js +4 -16
  88. package/lib/translations.js +3 -6
  89. package/lib/trust-badges.js +4 -7
  90. package/lib/vendor/MANIFEST.json +107 -97
  91. package/lib/vendor/blamejs/CHANGELOG.md +180 -0
  92. package/lib/vendor/blamejs/NOTICE +14 -10
  93. package/lib/vendor/blamejs/README.md +17 -5
  94. package/lib/vendor/blamejs/index.js +2 -0
  95. package/lib/vendor/blamejs/lib/acme.js +9 -9
  96. package/lib/vendor/blamejs/lib/api-snapshot.js +35 -16
  97. package/lib/vendor/blamejs/lib/app-shutdown.js +1 -1
  98. package/lib/vendor/blamejs/lib/archive-adapters.js +1 -1
  99. package/lib/vendor/blamejs/lib/archive.js +13 -24
  100. package/lib/vendor/blamejs/lib/auth/access-lock.js +2 -1
  101. package/lib/vendor/blamejs/lib/auth/bot-challenge.js +3 -6
  102. package/lib/vendor/blamejs/lib/auth/ciba.js +32 -3
  103. package/lib/vendor/blamejs/lib/auth/oauth.js +65 -8
  104. package/lib/vendor/blamejs/lib/auth/password.js +1 -1
  105. package/lib/vendor/blamejs/lib/auth/saml.js +21 -3
  106. package/lib/vendor/blamejs/lib/auth-bot-challenge.js +3 -3
  107. package/lib/vendor/blamejs/lib/backup/index.js +5 -5
  108. package/lib/vendor/blamejs/lib/case-fold-classes.js +491 -0
  109. package/lib/vendor/blamejs/lib/codepoint-class.js +131 -0
  110. package/lib/vendor/blamejs/lib/constants.js +79 -0
  111. package/lib/vendor/blamejs/lib/db-declare-view.js +8 -2
  112. package/lib/vendor/blamejs/lib/error-page.js +20 -1
  113. package/lib/vendor/blamejs/lib/external-db.js +5 -1
  114. package/lib/vendor/blamejs/lib/flag-evaluation-context.js +24 -6
  115. package/lib/vendor/blamejs/lib/flag.js +28 -2
  116. package/lib/vendor/blamejs/lib/framework-schema.js +14 -3
  117. package/lib/vendor/blamejs/lib/guard-regex.js +2345 -89
  118. package/lib/vendor/blamejs/lib/http-client.js +217 -40
  119. package/lib/vendor/blamejs/lib/mail-bimi.js +1 -1
  120. package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +3 -2
  121. package/lib/vendor/blamejs/lib/mail-dav.js +26 -26
  122. package/lib/vendor/blamejs/lib/mail-deploy.js +10 -10
  123. package/lib/vendor/blamejs/lib/mail-scan.js +3 -0
  124. package/lib/vendor/blamejs/lib/mail-store.js +7 -1
  125. package/lib/vendor/blamejs/lib/mail.js +5 -1
  126. package/lib/vendor/blamejs/lib/middleware/age-gate.js +3 -2
  127. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +1 -1
  128. package/lib/vendor/blamejs/lib/middleware/assetlinks.js +2 -1
  129. package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +3 -2
  130. package/lib/vendor/blamejs/lib/middleware/bearer-auth.js +2 -1
  131. package/lib/vendor/blamejs/lib/middleware/compression.js +1 -1
  132. package/lib/vendor/blamejs/lib/middleware/cors.js +1 -1
  133. package/lib/vendor/blamejs/lib/middleware/csp-report.js +5 -5
  134. package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +2 -1
  135. package/lib/vendor/blamejs/lib/middleware/daily-byte-quota.js +2 -2
  136. package/lib/vendor/blamejs/lib/middleware/deny-response.js +2 -1
  137. package/lib/vendor/blamejs/lib/middleware/dpop.js +2 -2
  138. package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +2 -1
  139. package/lib/vendor/blamejs/lib/middleware/flag-context.js +17 -0
  140. package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +10 -8
  141. package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +3 -2
  142. package/lib/vendor/blamejs/lib/middleware/require-aal.js +3 -2
  143. package/lib/vendor/blamejs/lib/middleware/require-auth.js +4 -3
  144. package/lib/vendor/blamejs/lib/middleware/require-bound-key.js +4 -3
  145. package/lib/vendor/blamejs/lib/middleware/require-content-type.js +3 -2
  146. package/lib/vendor/blamejs/lib/middleware/require-methods.js +3 -2
  147. package/lib/vendor/blamejs/lib/middleware/require-mtls.js +3 -2
  148. package/lib/vendor/blamejs/lib/middleware/security-txt.js +2 -1
  149. package/lib/vendor/blamejs/lib/middleware/web-app-manifest.js +2 -1
  150. package/lib/vendor/blamejs/lib/network-dns-resolver.js +1 -1
  151. package/lib/vendor/blamejs/lib/network-dns.js +3 -3
  152. package/lib/vendor/blamejs/lib/network-nts.js +35 -14
  153. package/lib/vendor/blamejs/lib/network-proxy.js +42 -4
  154. package/lib/vendor/blamejs/lib/network-smtp-policy.js +1 -1
  155. package/lib/vendor/blamejs/lib/network-tls.js +341 -11
  156. package/lib/vendor/blamejs/lib/nist-crosswalk.js +2 -2
  157. package/lib/vendor/blamejs/lib/ntp-check.js +30 -19
  158. package/lib/vendor/blamejs/lib/object-store/azure-blob.js +1 -1
  159. package/lib/vendor/blamejs/lib/object-store/gcs.js +1 -1
  160. package/lib/vendor/blamejs/lib/object-store/http-put.js +2 -1
  161. package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +5 -5
  162. package/lib/vendor/blamejs/lib/object-store/sigv4.js +2 -2
  163. package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +4 -2
  164. package/lib/vendor/blamejs/lib/openapi.js +1 -1
  165. package/lib/vendor/blamejs/lib/pqc-agent.js +4 -7
  166. package/lib/vendor/blamejs/lib/problem-details.js +2 -1
  167. package/lib/vendor/blamejs/lib/redis-client.js +12 -1
  168. package/lib/vendor/blamejs/lib/regex-linear.js +1349 -0
  169. package/lib/vendor/blamejs/lib/render.js +531 -7
  170. package/lib/vendor/blamejs/lib/request-helpers.js +181 -5
  171. package/lib/vendor/blamejs/lib/router.js +37 -13
  172. package/lib/vendor/blamejs/lib/safe-async.js +142 -0
  173. package/lib/vendor/blamejs/lib/safe-icap.js +3 -0
  174. package/lib/vendor/blamejs/lib/safe-sql.js +15 -3
  175. package/lib/vendor/blamejs/lib/self-update.js +2 -2
  176. package/lib/vendor/blamejs/lib/testing.js +54 -7
  177. package/lib/vendor/blamejs/lib/validate-opts.js +56 -0
  178. package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +62 -25
  179. package/lib/vendor/blamejs/lib/vendor/blamejs-pki.cjs +2004 -142
  180. package/lib/vendor/blamejs/lib/vendor/browser/noble-ciphers.mjs +727 -0
  181. package/lib/vendor/blamejs/lib/vendor/browser/noble-hashes.mjs +1070 -0
  182. package/lib/vendor/blamejs/lib/vendor/browser/noble-post-quantum.mjs +1090 -0
  183. package/lib/vendor/blamejs/lib/vendor/noble-post-quantum.cjs +510 -227
  184. package/lib/vendor/blamejs/lib/webhook-hmac.js +19 -2
  185. package/lib/vendor/blamejs/lib/webhook.js +1 -1
  186. package/lib/vendor/blamejs/lib/ws-client.js +24 -2
  187. package/lib/vendor/blamejs/package.json +1 -1
  188. package/lib/vendor/blamejs/sbom.cdx.json +6 -6
  189. package/lib/vendor-invoices.js +3 -6
  190. package/lib/vendors.js +5 -19
  191. package/lib/webhook-receiver.js +46 -83
  192. package/lib/webhook-subscriptions.js +3 -13
  193. package/lib/wishlist-sharing.js +5 -10
  194. package/package.json +1 -1
@@ -43,6 +43,7 @@
43
43
  // values (RFC 9110), not byte sizes. Names are RFC 9110 reason phrases;
44
44
  // every consumer reads HTTP_STATUS.<NAME> rather than the underlying
45
45
  // integer, so the hex form is purely an internal storage detail.
46
+ var C = require("./constants");
46
47
  var structuredFields = require("./structured-fields");
47
48
  var pick = require("./pick");
48
49
  var codepointClass = require("./codepoint-class");
@@ -245,8 +246,15 @@ function resolveActorWithOverride(callerOpts, baseOverride) {
245
246
  * for a security decision on an internet-facing listener. Prefer the
246
247
  * predicate form. Returns `null` when no address can be read — never throws.
247
248
  *
249
+ * `forwardedHeaders` names which header carries the address, in preference
250
+ * order — the first one PRESENT on the request is used (present rather than
251
+ * non-empty), and it defaults to
252
+ * `["x-forwarded-for"]`. It only has an effect alongside `trustProxy`; list
253
+ * only headers your proxy sets or overwrites (see `trustedClientIp`).
254
+ *
248
255
  * @opts
249
- * trustProxy: boolean | number | function // false (default) | predicate (peer-gated) | legacy true/hop-count
256
+ * trustProxy: boolean | number | function // false (default) | predicate (peer-gated) | legacy true/hop-count
257
+ * forwardedHeaders: string[] // header family, in order — default: ["x-forwarded-for"]
250
258
  *
251
259
  * @example
252
260
  * var req = {
@@ -268,6 +276,52 @@ function resolveActorWithOverride(callerOpts, baseOverride) {
268
276
  * b.requestHelpers.clientIp(undefined);
269
277
  * // → null
270
278
  */
279
+ // The forwarded-header family read when none is named. Unchanged default for
280
+ // every caller: X-Forwarded-For alone.
281
+ var DEFAULT_FORWARDED_HEADERS = Object.freeze(["x-forwarded-for"]);
282
+
283
+ // RFC 9110 5.1 field-name = token. An operator writes these at config time, so
284
+ // a typo (a stray space, a trailing colon copied out of a vendor doc) should
285
+ // surface at construction rather than silently matching no header and reading
286
+ // as "this deployment sends nothing".
287
+ var FORWARDED_HEADER_NAME_RE = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
288
+ // Length is bounded before the pattern runs; no real field name approaches it.
289
+ var MAX_FORWARDED_HEADER_NAME = 64;
290
+
291
+ // Normalise an operator-declared forwarded-header family to lowercase — node
292
+ // lowercases incoming header names, so a name written in the vendor's
293
+ // documented casing (CF-Connecting-IP) has to fold or it matches nothing.
294
+ function _normForwardedHeaders(value, where) {
295
+ if (value === undefined || value === null) return DEFAULT_FORWARDED_HEADERS;
296
+ if (!Array.isArray(value) || value.length === 0) {
297
+ throw new TypeError(where + ": forwardedHeaders must be a non-empty array of " +
298
+ "header names, got " + JSON.stringify(value));
299
+ }
300
+ var out = [];
301
+ for (var i = 0; i < value.length; i++) {
302
+ var name = value[i];
303
+ var bounded = typeof name === "string" &&
304
+ name.length > 0 && name.length <= MAX_FORWARDED_HEADER_NAME;
305
+ if (!bounded || !FORWARDED_HEADER_NAME_RE.test(name)) {
306
+ throw new TypeError(where + ": forwardedHeaders[" + i + "] is not a valid HTTP " +
307
+ "field name, got " + JSON.stringify(name));
308
+ }
309
+ out.push(name.toLowerCase());
310
+ }
311
+ return out;
312
+ }
313
+
314
+ // The non-throwing counterpart for the per-request path: a malformed family
315
+ // yields an EMPTY list, so no forwarded header is read and resolution falls
316
+ // back to the socket address. Returning the default family instead would
317
+ // honour X-Forwarded-For on a deployment whose operator asked for something
318
+ // else, which is the wrong way to fail.
319
+ function _normForwardedHeadersOrNone(value) {
320
+ if (value === undefined || value === null) return DEFAULT_FORWARDED_HEADERS;
321
+ try { return _normForwardedHeaders(value, "clientIp"); }
322
+ catch (_e) { return []; }
323
+ }
324
+
271
325
  function clientIp(req, opts) {
272
326
  if (!req) return null;
273
327
  var socketAddr =
@@ -276,7 +330,30 @@ function clientIp(req, opts) {
276
330
  : null;
277
331
  var trust = opts && opts.trustProxy;
278
332
  if (trust && req.headers) {
279
- var xff = req.headers["x-forwarded-for"];
333
+ // Which header carries the address is the operator's to declare; the trust
334
+ // decision below is not. The first header PRESENT on the request wins, so
335
+ // the order is the order they listed.
336
+ //
337
+ // This is a request-shape reader on the hot path, so a malformed family
338
+ // reads no forwarded header at all rather than throwing out of a live
339
+ // request — falling back to the socket address, the fail-closed direction.
340
+ // The construction-time path (`trustedClientIp`) is where a typo is a
341
+ // config mistake and is refused loudly.
342
+ var names = _normForwardedHeadersOrNone(opts && opts.forwardedHeaders);
343
+ var xff = null;
344
+ for (var n = 0; n < names.length; n++) {
345
+ var candidate = req.headers[names[n]];
346
+ // PRESENT decides, not non-empty. A header the operator listed first is
347
+ // the one they trust most; if their proxy set it and it came through
348
+ // empty, that says this request has no forwarded address — falling
349
+ // through to a lower-priority header would answer with one the client
350
+ // may have set instead, which is the wrong direction to resolve an
351
+ // ambiguity about who the caller is.
352
+ if (candidate !== undefined && candidate !== null) {
353
+ xff = candidate;
354
+ break;
355
+ }
356
+ }
280
357
  if (xff) {
281
358
  var hops = parseListHeader(xff);
282
359
  if (hops.length) {
@@ -333,17 +410,45 @@ function clientIp(req, opts) {
333
410
  * gate uses it to refuse a bare `trustProxy` at construction (fail closed).
334
411
  *
335
412
  * With `clientIpResolver(req)` the operator owns resolution entirely. With
336
- * `trustedProxies` (CIDRs of the reverse proxies), `X-Forwarded-For` is
413
+ * `trustedProxies` (CIDRs of the reverse proxies), the forwarded header is
337
414
  * honored only when the immediate peer is one of them. With neither, only
338
415
  * the socket address is used and forwarded headers are ignored.
339
416
  *
417
+ * `forwardedHeaders` names which header carries the address, in preference
418
+ * order — the first one PRESENT on the request is used, present rather than
419
+ * non-empty: a first-listed header that arrives empty says this request
420
+ * carries no forwarded address, rather than deferring to a lower-priority
421
+ * one the client may have set. It defaults to
422
+ * `["x-forwarded-for"]`. Cloudflare publishes the client address as
423
+ * `CF-Connecting-IP` and the common nginx recipe
424
+ * (`proxy_set_header X-Real-IP $remote_addr`) as `X-Real-IP`, so a deployment
425
+ * behind either had no way to use this resolver at all: reading the header
426
+ * directly drops the peer gate, and `clientIpResolver` hands back the whole
427
+ * trust decision — the CIDR matching, the IPv4-mapped-IPv6 folding — while
428
+ * still reporting `peerGated`. Every listed header is parsed the same way, so
429
+ * a single-address header is simply a one-hop chain.
430
+ *
431
+ * List ONLY headers your proxy sets or overwrites on every request. The peer
432
+ * gate proves the request arrived THROUGH your proxy; it cannot prove your
433
+ * proxy authored the header. A proxy that passes an unknown header through
434
+ * unchanged lets a client inject it, and naming that header here would honor
435
+ * the injected value — which is why the default stays the single header the
436
+ * chain walk was designed for.
437
+ *
340
438
  * @opts
341
- * trustedProxies: string | string[], // CIDRs — peer-gate X-Forwarded-For
439
+ * trustedProxies: string | string[], // CIDRs — peer-gate the forwarded header
440
+ * forwardedHeaders: string[], // header family, in order — default: ["x-forwarded-for"]
342
441
  * clientIpResolver: function(req): string|null, // own resolution entirely
343
442
  *
344
443
  * @example
345
444
  * var tip = b.requestHelpers.trustedClientIp({ trustedProxies: ["10.0.0.0/8"] });
346
445
  * var ip = tip.resolve(req); // peer-gated; forged XFF from a direct caller ignored
446
+ *
447
+ * var cf = b.requestHelpers.trustedClientIp({
448
+ * trustedProxies: ["10.0.0.0/8"],
449
+ * forwardedHeaders: ["cf-connecting-ip"],
450
+ * });
451
+ * cf.resolve(req); // same gate, the header the edge actually sets
347
452
  */
348
453
  // Build the trusted-proxy predicate shared by trustedClientIp / trustedProtocol.
349
454
  // Validates each CIDR (a CIDR is valid iff it contains its own network address,
@@ -387,11 +492,15 @@ function trustedClientIp(opts) {
387
492
  throw new TypeError("trustedClientIp: clientIpResolver must be a function(req) => ip|null");
388
493
  }
389
494
  var predicate = _trustedProxyPredicate(_normTrustedProxies(opts), "trustedClientIp");
495
+ // Validated whether or not a peer gate is configured — a malformed family is
496
+ // an operator typo either way, and it should not wait for the deployment
497
+ // that finally turns the gate on to surface.
498
+ var forwardedHeaders = _normForwardedHeaders(opts.forwardedHeaders, "trustedClientIp");
390
499
  return {
391
500
  peerGated: !!(resolver || predicate),
392
501
  resolve: function (req) {
393
502
  if (resolver) return resolver(req);
394
- if (predicate) return clientIp(req, { trustProxy: predicate });
503
+ if (predicate) return clientIp(req, { trustProxy: predicate, forwardedHeaders: forwardedHeaders });
395
504
  return clientIp(req, { trustProxy: false });
396
505
  },
397
506
  };
@@ -1480,7 +1589,74 @@ function makeResourceAuditEmitter(sink, resourceKind, idFor) {
1480
1589
  };
1481
1590
  }
1482
1591
 
1592
+ /**
1593
+ * @primitive b.requestHelpers.failAfterHeaders
1594
+ * @signature b.requestHelpers.failAfterHeaders(res)
1595
+ * @since 0.18.19
1596
+ * @status stable
1597
+ * @related b.render.stream, b.errorPage.create
1598
+ *
1599
+ * End a response that has already sent its status line, in a way that tells the
1600
+ * client the transfer is incomplete. Returns `true` when it handled the
1601
+ * response, `false` when the caller still owns it and can write a normal error.
1602
+ *
1603
+ * Once the headers are on the wire they cannot be replaced, and every attempt
1604
+ * to do so makes things worse rather than better: `writeHead` throws
1605
+ * `ERR_HTTP_HEADERS_SENT`, and a caller that catches that and falls back to
1606
+ * `res.end("Internal Server Error")` appends those words to whatever partial
1607
+ * body the client already has. The client then sees a 200 with a plausible
1608
+ * final row. Silent truncation presented as success is a data-integrity
1609
+ * failure, so the honest signal is an incomplete transfer.
1610
+ *
1611
+ * Which signal that is depends on the protocol, which is why this is one
1612
+ * primitive rather than a line repeated at each error path:
1613
+ *
1614
+ * - **HTTP/1.1** — destroy the socket, so a chunked response ends without its
1615
+ * terminating chunk and the client reports a failed download.
1616
+ * - **HTTP/2** — destroying with no argument closes the stream with
1617
+ * `RST_STREAM(NO_ERROR)`, which a client reads as a clean end: the truncated
1618
+ * body arrives as a complete 200. The stream is closed with
1619
+ * `INTERNAL_ERROR` instead so the failure survives the protocol.
1620
+ * - **A response that cannot carry a body** — HEAD, 204, 304 — is already
1621
+ * complete once its headers are sent. There is nothing to truncate, so it is
1622
+ * ended normally; destroying would throw away a valid response.
1623
+ *
1624
+ * @example
1625
+ * if (!b.requestHelpers.failAfterHeaders(res)) {
1626
+ * b.render.json(res, { error: "internal" }, { status: 500 });
1627
+ * }
1628
+ */
1629
+ var H2_INTERNAL_ERROR = 0x02; // RFC 9113 §7
1630
+
1631
+ function failAfterHeaders(res) {
1632
+ if (!res) return true;
1633
+ if (res.writableEnded === true || res.destroyed === true) return true;
1634
+ if (res.headersSent !== true) return false; // the caller still owns it
1635
+ try {
1636
+ // A bodiless response is complete the moment its headers are sent, so
1637
+ // there is nothing a truncation signal could mean — destroying one throws
1638
+ // away a response that was whole. Which statuses those are is a rule of the
1639
+ // protocol rather than of this function, so it is asked rather than
1640
+ // restated; `_hasBody` covers the other case, a response to HEAD, which is
1641
+ // a property of the REQUEST and so not something a status can answer.
1642
+ if (res._hasBody === false ||
1643
+ (typeof res.statusCode === "number" && C.HTTP.bodiless(res.statusCode))) {
1644
+ if (typeof res.end === "function") res.end();
1645
+ return true;
1646
+ }
1647
+ // HTTP/2: a bare destroy closes with NO_ERROR, which reads as a clean end.
1648
+ if (res.stream && typeof res.stream.close === "function") {
1649
+ res.stream.close(H2_INTERNAL_ERROR);
1650
+ return true;
1651
+ }
1652
+ if (typeof res.destroy === "function") { res.destroy(); return true; }
1653
+ if (typeof res.end === "function") res.end();
1654
+ } catch (_e) { /* socket already gone */ }
1655
+ return true;
1656
+ }
1657
+
1483
1658
  module.exports = {
1659
+ failAfterHeaders: failAfterHeaders,
1484
1660
  resolveRoute: resolveRoute,
1485
1661
  makeResourceAuditEmitter: makeResourceAuditEmitter,
1486
1662
  makeSkipMatcher: makeSkipMatcher,
@@ -56,6 +56,10 @@ var audit = lazyRequire(function () { return require("./audit"); });
56
56
  // the operator's `b.compliance.set(...)` runs; the posture lookup only
57
57
  // matters at listen() time, well after boot finishes.
58
58
  var compliance = lazyRequire(function () { return require("./compliance"); });
59
+ // network-tls — lazy for the same reason as compliance: it is only consulted
60
+ // at listen() time, and loading the trust store during router's own require
61
+ // would pull it in on every boot that never serves TLS.
62
+ var networkTls = lazyRequire(function () { return require("./network-tls"); });
59
63
 
60
64
  var log = boot("router");
61
65
  var HTTP_STATUS = requestHelpers.HTTP_STATUS;
@@ -67,6 +71,18 @@ var HTTP_STATUS = requestHelpers.HTTP_STATUS;
67
71
  // even when Node's nghttp2 vendor lags the upstream fix: tag every
68
72
  // session with `_blamejsGoawaySent` on the framework's GOAWAY emission,
69
73
  // and force-destroy on any subsequent frame activity.
74
+ // The last thing a request handler can do when everything above it has failed.
75
+ // Writing a status is only possible while the headers are still ours; past
76
+ // that, the honest answer is an incomplete transfer, which failAfterHeaders
77
+ // produces correctly for HTTP/1.1, HTTP/2 and bodiless responses alike.
78
+ function _lastResortError(res) {
79
+ try {
80
+ if (requestHelpers.failAfterHeaders(res)) return;
81
+ res.writeHead(HTTP_STATUS.INTERNAL_SERVER_ERROR, { "Content-Type": "text/plain" });
82
+ res.end("Internal Server Error");
83
+ } catch (_e) { /* the connection is already gone; nothing left to say */ }
84
+ }
85
+
70
86
  var WINDOW_UPDATE_FRAME_TYPE = 0x8; // RFC 7540 §6.9 frame type
71
87
  // Per-stream WINDOW_UPDATE rate cap. Above this rate the framework
72
88
  // destroys the stream; legitimate clients never burst this fast on a
@@ -971,7 +987,7 @@ class Router {
971
987
  metadata: { reason: "posture-refuse", method: req.method, url: req.url },
972
988
  });
973
989
  } catch (_e) { /* audit best-effort */ }
974
- return { status: 425, reason: "early-data-refused" };
990
+ return { status: C.HTTP.STATUS.TOO_EARLY, reason: "early-data-refused" };
975
991
  }
976
992
  // posture === "replay-cache" — dedupe by SHA3-512 within the rolling
977
993
  // window. Hash inputs (method + url + Host + Authorization + bound
@@ -999,7 +1015,7 @@ class Router {
999
1015
  windowMs: TLS_0RTT_REPLAY_WINDOW_MS },
1000
1016
  });
1001
1017
  } catch (_e) { /* audit best-effort */ }
1002
- return { status: 425, reason: "early-data-replay" };
1018
+ return { status: C.HTTP.STATUS.TOO_EARLY, reason: "early-data-replay" };
1003
1019
  }
1004
1020
  // Bounded entry count — when the cache hits the cap, drop the
1005
1021
  // oldest entries to make room. The reap pass already ran above.
@@ -1041,7 +1057,7 @@ class Router {
1041
1057
  if (verdict0Rtt) {
1042
1058
  // RFC 8470 §5 — 425 Too Early. Connection: close so the peer
1043
1059
  // cannot reuse the session ticket on the next attempt.
1044
- res.writeHead(425, {
1060
+ res.writeHead(C.HTTP.STATUS.TOO_EARLY, {
1045
1061
  "Content-Type": "text/plain; charset=utf-8",
1046
1062
  "Connection": "close",
1047
1063
  });
@@ -1094,7 +1110,7 @@ class Router {
1094
1110
  if (url.charAt(0) === "/" &&
1095
1111
  url.charAt(1) !== "/" && url.charAt(1) !== "\\") {
1096
1112
  // 302 Found — RFC 7231 §6.4.3. Not in HTTP_STATUS table.
1097
- res.writeHead(302, { Location: url });
1113
+ res.writeHead(C.HTTP.STATUS.FOUND, { Location: url });
1098
1114
  res.end();
1099
1115
  return;
1100
1116
  }
@@ -1153,7 +1169,7 @@ class Router {
1153
1169
  metadata: { target: url, origin: targetOrigin },
1154
1170
  });
1155
1171
  } catch (_e) { /* audit best-effort */ }
1156
- res.writeHead(302, { Location: url });
1172
+ res.writeHead(C.HTTP.STATUS.FOUND, { Location: url });
1157
1173
  res.end();
1158
1174
  };
1159
1175
  res.status = (code) => {
@@ -1164,16 +1180,18 @@ class Router {
1164
1180
  self.handle(req, res).catch((err) => {
1165
1181
  log.error("route error: " + req.method + " " + req.url + " " + err.message + " " +
1166
1182
  (err.stack ? err.stack.split("\n").slice(0, 5).join(" | ") : ""));
1183
+ // `writableEnded` is false for a response that is mid-body, so this
1184
+ // fallback used to call writeHead on a response whose headers were
1185
+ // already sent. That throws ERR_HTTP_HEADERS_SENT — from inside this
1186
+ // .catch, where nothing handles it — and the unhandled rejection takes
1187
+ // the whole process down, along with every other in-flight request.
1188
+ // One route whose producer fails after the first byte was enough.
1167
1189
  if (self.errorHandler) {
1168
1190
  try { self.errorHandler(err, req, res); } catch (_) {
1169
- if (!res.writableEnded) {
1170
- res.writeHead(HTTP_STATUS.INTERNAL_SERVER_ERROR, { "Content-Type": "text/plain" });
1171
- res.end("Internal Server Error");
1172
- }
1191
+ _lastResortError(res);
1173
1192
  }
1174
- } else if (!res.writableEnded) {
1175
- res.writeHead(HTTP_STATUS.INTERNAL_SERVER_ERROR, { "Content-Type": "text/plain" });
1176
- res.end("Internal Server Error");
1193
+ } else {
1194
+ _lastResortError(res);
1177
1195
  }
1178
1196
  });
1179
1197
  };
@@ -1262,7 +1280,13 @@ class Router {
1262
1280
  // an empty list — Node rejects an unknown option, and "no algorithms"
1263
1281
  // and "extension disabled" are the same wire behaviour anyway.
1264
1282
  if (certCompression.length > 0) h2Defaults.certificateCompression = certCompression;
1265
- server = http2.createSecureServer(Object.assign(h2Defaults, tlsOptions), requestHandler);
1283
+ // An operator serving legacy clients caps the listener below TLS 1.3;
1284
+ // certificate compression is a TLS 1.3 extension and Node refuses the
1285
+ // whole options object rather than ignoring it, so the listener would
1286
+ // die at bind over an option they never asked for.
1287
+ var h2Opts = networkTls()._stripUnreachableCertCompression(
1288
+ Object.assign(h2Defaults, tlsOptions), tlsOptions);
1289
+ server = http2.createSecureServer(h2Opts, requestHandler);
1266
1290
 
1267
1291
  // CVE-2026-21714 — H/2 WINDOW_UPDATE leak after GOAWAY. nghttp2
1268
1292
  // holds per-stream flow-control state after GOAWAY; late-arriving
@@ -245,6 +245,147 @@ function withSignal(promise, signal) {
245
245
  // exits even with pending awaits — the unref'd timer is not enough to
246
246
  // hold the loop alive, so the awaiting promise never resolves.)
247
247
  //
248
+ /**
249
+ * @primitive b.safeAsync.writeChunk
250
+ * @signature b.safeAsync.writeChunk(writable, chunk)
251
+ * @since 0.18.19
252
+ * @status stable
253
+ * @related b.render.stream, b.safeAsync.safeAwait
254
+ *
255
+ * Write one chunk to a `Writable` and resolve when it has been accepted,
256
+ * waiting for `'drain'` when the stream says it is full.
257
+ *
258
+ * `write()` returning `false` is easy to discard, and nothing appears to break
259
+ * when you do: a local client drains instantly, so the queue never grows in
260
+ * testing. Under a slow client Node keeps buffering in memory and a
261
+ * deliberately bounded-memory export becomes unbounded.
262
+ *
263
+ * Waiting is not enough on its own, either. A closed socket never emits
264
+ * `'drain'`, so a loop that always awaits it hangs the request forever — this
265
+ * settles on `'error'` and `'close'` as well, so a disconnected peer rejects
266
+ * rather than stalls. Every listener it adds is removed on the way out,
267
+ * whichever event wins.
268
+ *
269
+ * @example
270
+ * for await (var row of rows) {
271
+ * await b.safeAsync.writeChunk(res, row + "\n");
272
+ * }
273
+ */
274
+ function _swallow() {}
275
+
276
+ // Does this sink report the outcome of a write through the callback the stream
277
+ // contract gives it? Asked of the object, not of how its `write` happens to be
278
+ // declared — a wrapper written as `write(chunk, ...args)` supports the callback
279
+ // exactly as one written out in full does, and counting parameters would say
280
+ // otherwise. Every Node writable and every HTTP response carries the buffered
281
+ // length; a hand-rolled response double does not, and is answered on the spot.
282
+ function _reportsCompletion(writable) {
283
+ if (!writable || typeof writable.write !== "function") return false;
284
+ return writable.writableLength !== undefined || typeof writable._write === "function";
285
+ }
286
+
287
+ // Take delivery of the ONE `error` a failing sink is about to emit, and only
288
+ // when nothing else is listening for it — a caller with its own handler keeps
289
+ // seeing it, and an unrelated error later still surfaces.
290
+ function _absorbOneError(writable) {
291
+ if (!writable || typeof writable.once !== "function" ||
292
+ typeof writable.removeListener !== "function") return;
293
+ if (typeof writable.listenerCount === "function" &&
294
+ writable.listenerCount("error") > 0) return;
295
+ writable.once("error", _swallow);
296
+ setImmediate(function () { writable.removeListener("error", _swallow); });
297
+ }
298
+
299
+ function writeChunk(writable, chunk) {
300
+ return new Promise(function (resolve, reject) {
301
+ if (!writable || typeof writable.write !== "function") {
302
+ reject(new SafeAsyncError(
303
+ "safeAsync.writeChunk: expected a writable with a write() method",
304
+ "async/bad-writable"));
305
+ return;
306
+ }
307
+ // Already gone before the call. `close` is one-shot, so a listener
308
+ // attached now would never fire; `write()` on a destroyed stream returns
309
+ // false without re-emitting `error`; and `drain` never comes. Every one of
310
+ // those is a wait that outlives the request — the archive writer pinned a
311
+ // handler, its entry list and its source streams for the life of the
312
+ // process this way, against a client that had already hung up.
313
+ if (writable.destroyed === true || writable.closed === true ||
314
+ writable.writableEnded === true) {
315
+ reject(new SafeAsyncError(
316
+ "safeAsync.writeChunk: the stream was already closed",
317
+ "async/writable-closed"));
318
+ return;
319
+ }
320
+ var settled = false;
321
+ function done(err, absorbFollowingError) {
322
+ if (settled) return;
323
+ settled = true;
324
+ if (typeof writable.removeListener === "function") {
325
+ writable.removeListener("drain", onDrain);
326
+ writable.removeListener("error", onError);
327
+ writable.removeListener("close", onClose);
328
+ }
329
+ if (err) {
330
+ // Only when the sink told us through the write callback: it emits its
331
+ // own `error` right afterwards, and with nobody else listening that
332
+ // event ends the process — though the failure is already on its way to
333
+ // the caller through this promise. When the failure ARRIVED as an
334
+ // `error` event there is no second one coming, and arming for one would
335
+ // swallow an unrelated failure that happened to follow it.
336
+ if (absorbFollowingError) _absorbOneError(writable);
337
+ reject(err);
338
+ return;
339
+ }
340
+ resolve();
341
+ }
342
+ function onDrain() { done(null); }
343
+ function onError(e) { done(e); }
344
+ function onClose() {
345
+ done(new SafeAsyncError(
346
+ "safeAsync.writeChunk: the stream closed before it drained",
347
+ "async/writable-closed"));
348
+ }
349
+ // A stream that took the chunk has not necessarily written it. `write()`
350
+ // returning true only means there is room for more; the sink reports the
351
+ // outcome through the callback, and a failure there arrives as an `error`
352
+ // event afterwards. Resolving on the return value alone dropped every
353
+ // listener before that event, leaving it unhandled — which by default ends
354
+ // the process, so an archive whose destination failed part-way through
355
+ // took the server down instead of failing the download.
356
+ // A sink may answer before `write()` has even returned, and until it does
357
+ // there is no telling whether there is room for more. So a callback that
358
+ // arrives first is remembered and read below — settling on it here would
359
+ // resolve a write that still owes a `drain`, and leave every listener
360
+ // attached afterwards with nothing left to remove them.
361
+ var accepted;
362
+ var writeReturned = false;
363
+ var answered = false;
364
+ var answeredWith = null;
365
+ var reportsCompletion = _reportsCompletion(writable);
366
+ function onWritten(err) {
367
+ answered = true;
368
+ answeredWith = err || null;
369
+ if (!writeReturned) return;
370
+ if (err) { done(err, true); return; }
371
+ if (accepted !== false) done(null); // written, and there is room
372
+ }
373
+ try {
374
+ accepted = reportsCompletion ? writable.write(chunk, onWritten) : writable.write(chunk);
375
+ } catch (e) { done(e); return; }
376
+ writeReturned = true;
377
+ if (answered) {
378
+ if (answeredWith) { done(answeredWith, true); return; }
379
+ if (accepted !== false) { done(null); return; } // otherwise a drain is still owed
380
+ }
381
+ if (typeof writable.once !== "function") { done(null); return; }
382
+ if (accepted !== false && !reportsCompletion) { done(null); return; }
383
+ writable.once("error", onError);
384
+ writable.once("close", onClose);
385
+ if (accepted === false) writable.once("drain", onDrain);
386
+ });
387
+ }
388
+
248
389
  // ms <= 0 resolves immediately (matches setTimeout's clamp-to-1ms but
249
390
  // without the wasted tick). Non-finite ms rejects.
250
391
 
@@ -1662,6 +1803,7 @@ var CircuitBreaker = retryHelper.CircuitBreaker;
1662
1803
  module.exports = {
1663
1804
  withTimeout: withTimeout,
1664
1805
  withSignal: withSignal,
1806
+ writeChunk: writeChunk,
1665
1807
  withTimeoutSignal: withTimeoutSignal,
1666
1808
  sleep: sleep,
1667
1809
  repeating: repeating,
@@ -460,6 +460,9 @@ function _detectThreat(statusCode, headers) {
460
460
  // hit, or 200 + the modified-message with X-Infection-Found set.
461
461
  var found = false;
462
462
  var name;
463
+ // Deliberately the number: an ICAP status line carries ICAP's own codes,
464
+ // which merely look like HTTP's. Naming this through C.HTTP.STATUS would say
465
+ // "HTTP" about a protocol that is not HTTP.
463
466
  if (statusCode === 403) found = true;
464
467
  var inf = _firstHeader(headers["x-infection-found"]);
465
468
  if (typeof inf === "string" && inf.length > 0) {
@@ -222,7 +222,7 @@ function quoteIdentifier(name, dialect, opts) {
222
222
 
223
223
  /**
224
224
  * @primitive b.safeSql.quoteQualified
225
- * @signature b.safeSql.quoteQualified(parts, dialect?)
225
+ * @signature b.safeSql.quoteQualified(parts, dialect?, opts?)
226
226
  * @since 0.1.0
227
227
  * @status stable
228
228
  * @related b.safeSql.quoteIdentifier, b.safeSql.validateIdentifier
@@ -235,6 +235,18 @@ function quoteIdentifier(name, dialect, opts) {
235
235
  * usually-nonexistent object). Accepts an array of parts OR a
236
236
  * dot-separated string.
237
237
  *
238
+ * `opts` is forwarded to `quoteIdentifier` for every segment, so
239
+ * `{ allowReserved: true }` reaches a qualified name the same way it
240
+ * reaches a bare one. Without it this was the stricter of the two
241
+ * siblings with no way to opt out, and a view in a schema legitimately
242
+ * named after a keyword was refused here while `b.db.from()` queried
243
+ * it.
244
+ *
245
+ * @opts
246
+ * allowReserved: boolean, // default: false — permit SQL-keyword names in
247
+ * // ANY segment (safe once quoted); forwarded to
248
+ * // quoteIdentifier for each one
249
+ *
238
250
  * @example
239
251
  * var b = require("blamejs");
240
252
  * b.safeSql.quoteQualified(["public", "users"]);
@@ -249,7 +261,7 @@ function quoteIdentifier(name, dialect, opts) {
249
261
  * b.safeSql.quoteQualified(["app", "orders"], "mysql");
250
262
  * // → "`app`.`orders`"
251
263
  */
252
- function quoteQualified(parts, dialect) {
264
+ function quoteQualified(parts, dialect, opts) {
253
265
  var arr;
254
266
  if (typeof parts === "string") {
255
267
  if (parts.length === 0) {
@@ -269,7 +281,7 @@ function quoteQualified(parts, dialect) {
269
281
  }
270
282
  var quoted = [];
271
283
  for (var i = 0; i < arr.length; i++) {
272
- quoted.push(quoteIdentifier(arr[i], dialect));
284
+ quoted.push(quoteIdentifier(arr[i], dialect, opts));
273
285
  }
274
286
  return quoted.join(".");
275
287
  }
@@ -526,7 +526,7 @@ async function poll(opts) {
526
526
  "selfUpdate.poll: request failed: " + ((e && e.message) || String(e)));
527
527
  }
528
528
 
529
- if (res.statusCode === 304) { // HTTP status code (RFC 7232), not bytes
529
+ if (res.statusCode === C.HTTP.STATUS.NOT_MODIFIED) { // HTTP status code (RFC 7232), not bytes
530
530
  _safeAuditEmit("selfupdate.poll.checked", "success", {
531
531
  releasesUrl: opts.releasesUrl,
532
532
  currentVersion: opts.currentVersion,
@@ -534,7 +534,7 @@ async function poll(opts) {
534
534
  etagHit: true,
535
535
  });
536
536
  return { available: false, latestTag: null, currentVersion: opts.currentVersion,
537
- asset: null, signature: null, etag: opts.etag, statusCode: 304 }; // HTTP status code (RFC 7232), not bytes
537
+ asset: null, signature: null, etag: opts.etag, statusCode: C.HTTP.STATUS.NOT_MODIFIED }; // HTTP status code (RFC 7232), not bytes
538
538
  }
539
539
  if (res.statusCode < 200 || res.statusCode >= 300) {
540
540
  _safeAuditEmit("selfupdate.poll.checked", "denied", {