@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
@@ -30,16 +30,41 @@
30
30
  * Server-side HTML / JSON / XML response helpers.
31
31
  */
32
32
 
33
+ var C = require("./constants");
34
+ var lazyRequire = require("./lazy-require");
33
35
  var validateOpts = require("./validate-opts");
34
36
 
37
+ // safe-async — lazy because render is required during boot by the router and
38
+ // only the streaming helper needs it, so an operator who never streams does
39
+ // not pull the async toolkit in at load.
40
+ var safeAsync = lazyRequire(function () { return require("./safe-async"); });
41
+ // request-helpers — lazy for the same reason, and because it requires render's
42
+ // siblings; only the streaming and error paths need it.
43
+ var requestHelpers = lazyRequire(function () { return require("./request-helpers"); });
44
+
35
45
  var DEFAULT_CHARSET = "utf-8";
36
46
 
37
47
  function _alreadyDone(res) {
38
48
  return res && res.writableEnded === true;
39
49
  }
40
50
 
51
+ // The status to send. A status that was NOT given takes the default; one that
52
+ // was is sent exactly as given, so `writeHead` reports a nonsense value rather
53
+ // than this quietly turning it into a 200. `0` and `NaN` are the pair that
54
+ // makes the difference: both are falsy, and defaulting them away would answer
55
+ // a misconfigured handler with a successful response.
56
+ function _statusOr(opts, fallback) {
57
+ if (!opts || opts.status === undefined || opts.status === null) return fallback;
58
+ return opts.status;
59
+ }
60
+
41
61
  function _writeResponse(res, status, headers, body) {
42
62
  if (_alreadyDone(res)) return;
63
+ // The headers are already on the wire — writeHead would throw
64
+ // ERR_HTTP_HEADERS_SENT and take the caller's error path with it. This is
65
+ // the natural recovery after a streaming failure ("catch, then render a
66
+ // 500"), so it has to fail as an incomplete transfer rather than a crash.
67
+ if (res.headersSent === true && requestHelpers().failAfterHeaders(res)) return;
43
68
  if (typeof res.writeHead === "function") {
44
69
  res.writeHead(status, headers);
45
70
  } else {
@@ -62,6 +87,43 @@ function _mergedHeaders(base, extra) {
62
87
  return out;
63
88
  }
64
89
 
90
+ // `writeHead(status, headers)` merges with what `setHeader` recorded but lets
91
+ // its own object win on a name they share, so a default here replaced whatever a
92
+ // route had already said — silently, and only for the names the defaults happen
93
+ // to carry, so a `Content-Disposition` survived while the `Content-Type` beside
94
+ // it did not.
95
+ //
96
+ // Exactly one default steps aside for that, and only in `stream`. The reason is
97
+ // what the header MEANS in each place. `json`, `text` and `htmlString` encode
98
+ // the body themselves, so their `Content-Type` describes the bytes they just
99
+ // produced — it belongs with `Content-Length`, not with a preference, and
100
+ // inheriting an earlier one is how a JSON error body comes to be served as
101
+ // `text/html` and a reflected value in it becomes markup. `stream` does not know
102
+ // what its bytes are: `application/octet-stream` is a placeholder for an answer
103
+ // the caller has, which is exactly the case the route stating `text/csv` was
104
+ // giving it.
105
+ //
106
+ // `Cache-Control` never steps aside either, in any of them. It is a security
107
+ // default rather than a formatting one — dynamic responses must revalidate —
108
+ // and a default that any earlier `setHeader` in the chain could relax would not
109
+ // be a default. `opts.headers` remains the way to say otherwise, deliberately,
110
+ // because that says it at the call rather than somewhere up the middleware.
111
+ function _defaultContentTypeUnlessStated(res, defaults) {
112
+ if (!res || typeof res.getHeader !== "function") return defaults;
113
+ var stated = res.getHeader("Content-Type");
114
+ // A header set to nothing states nothing. `setHeader` also accepts an ARRAY,
115
+ // and a wrapper that keeps every header that way would have looked like it
116
+ // had said nothing at all.
117
+ if (Array.isArray(stated)) stated = stated.length ? stated[0] : "";
118
+ if (typeof stated === "number") stated = String(stated);
119
+ if (typeof stated !== "string" || stated === "") return defaults;
120
+ var out = {};
121
+ Object.keys(defaults).forEach(function (name) {
122
+ if (name !== "Content-Type") out[name] = defaults[name];
123
+ });
124
+ return out;
125
+ }
126
+
65
127
  // Default Cache-Control for dynamic responses. Browsers heuristically
66
128
  // cache HTML responses without explicit headers, which causes "saved
67
129
  // changes don't appear" bugs after a POST/redirect. `no-cache` permits
@@ -83,8 +145,13 @@ var DEFAULT_DYNAMIC_CACHE_CONTROL = "private, no-cache, must-revalidate";
83
145
  * and the dynamic-response Cache-Control. Status defaults to 200;
84
146
  * any custom headers in `opts.headers` merge over the defaults so
85
147
  * operators can pin a different Cache-Control or add CORS headers
86
- * without losing Content-Type. Returns `undefined` the response
87
- * is fully written by the time the call returns.
148
+ * without losing Content-Type. The Content-Type it sends is its own:
149
+ * this helper encodes the body, so the type describes the bytes it
150
+ * produced rather than a preference, and a `text/html` left on the
151
+ * response by an earlier `res.setHeader` does not carry over — that
152
+ * is how a JSON error body comes to be parsed as markup. Say it in
153
+ * `opts.headers` to send something else. Returns `undefined` — the
154
+ * response is fully written by the time the call returns.
88
155
  *
89
156
  * `opts.replacer` is forwarded to `JSON.stringify` (ECMA-262 §25.5.2,
90
157
  * the second argument) so handlers can serialize values that have no
@@ -120,7 +187,462 @@ function json(res, body, opts) {
120
187
  "Content-Length": Buffer.byteLength(encoded, "utf8"),
121
188
  "Cache-Control": DEFAULT_DYNAMIC_CACHE_CONTROL,
122
189
  }, opts.headers);
123
- _writeResponse(res, opts.status || 200, headers, encoded);
190
+ _writeResponse(res, _statusOr(opts, C.HTTP.STATUS.OK), headers, encoded);
191
+ }
192
+
193
+ /**
194
+ * @primitive b.render.stream
195
+ * @signature b.render.stream(res, iterable, opts?)
196
+ * @since 0.18.19
197
+ * @status stable
198
+ * @related b.render.json, b.safeAsync.writeChunk
199
+ *
200
+ * Write an async (or sync) iterable to a response, one chunk at a time, and
201
+ * end it. For a generated download — a CSV export, an NDJSON dump, a receipt —
202
+ * where the source is a generator over a cursor rather than a `Readable` that
203
+ * could simply be piped.
204
+ *
205
+ * The obvious loop is wrong in three ways that testing does not surface.
206
+ * `res.write()` returning `false` is easy to discard, and a local client
207
+ * drains instantly, so a bounded-memory export becomes unbounded only under a
208
+ * slow client. Always awaiting `'drain'` then hangs forever when the peer has
209
+ * gone, because a closed socket never emits it. And a producer that throws
210
+ * after the first byte cannot be turned into an error page: the status line is
211
+ * already sent, so the handler appends its message to the partial body and the
212
+ * client receives a 200 whose last row reads "Internal Server Error" — a
213
+ * truncated export that every consumer reads as complete.
214
+ *
215
+ * So: back-pressure is awaited, a closed peer stops the loop instead of
216
+ * stalling it, and a mid-stream throw destroys the connection. Destroying ends
217
+ * a chunked response without its terminating chunk, which is the only signal
218
+ * left that says "this transfer is incomplete" once bytes are on the wire. The
219
+ * error is re-thrown either way, so the caller still logs it.
220
+ *
221
+ * A producer that fails BEFORE yielding anything is not a truncated export —
222
+ * nothing was produced. Opening an async generator runs none of its body, so
223
+ * the first value is fetched while the status line is still unsent: a query
224
+ * that fails to run reaches the caller with a response it can still render an
225
+ * error page on, rather than as a download that dies partway. One row is
226
+ * therefore fetched before the headers go out, which is what any implementation
227
+ * must do to know whether the producer can produce at all.
228
+ *
229
+ * That wait belongs to a response THIS call commits. A long-lived stream whose
230
+ * first value is minutes away — an event subscription, a tail — should send its
231
+ * own head first (`res.writeHead(...)`, `res.flushHeaders()`) and then stream
232
+ * into it: with the status line already out there is nothing to hold back, so
233
+ * no value is fetched before the headers and the client sees the connection
234
+ * establish at once.
235
+ *
236
+ * Headers are written from `opts` before the first chunk unless the caller has
237
+ * already sent them. The `application/octet-stream` default steps aside for a
238
+ * Content-Type the route already set with `res.setHeader` — unlike the other
239
+ * helpers this one does not encode the body, so that default is a placeholder
240
+ * for an answer the caller has, and a route serving a CSV had already given it.
241
+ * An explicit `opts.headers` entry still wins over both, and the Cache-Control
242
+ * default does not step aside for anything but that. Nothing is buffered: a
243
+ * chunk is handed to the socket as the producer yields it.
244
+ *
245
+ * Pass a function instead of an iterable to be handed a signal that aborts when
246
+ * the client goes away, the caller aborts, or the stream fails. Cancelling a
247
+ * generator does not reach work it is already waiting on — a generator parked
248
+ * in `await query()` runs its `finally` only once that query returns on its
249
+ * own — so a producer holding a cursor, a connection or a file handle should
250
+ * take the signal and cancel with it.
251
+ *
252
+ * @opts
253
+ * status: 200, // numeric HTTP status, sent with the first chunk
254
+ * headers: {}, // merged over the dynamic-response defaults
255
+ * onError: "destroy", // "destroy" (default) or "rethrow" to leave the socket alone
256
+ * signal: AbortSignal, // stop producing when it aborts
257
+ *
258
+ * @example
259
+ * await b.render.stream(res, rows(), {
260
+ * headers: { "Content-Type": "text/csv; charset=utf-8" },
261
+ * });
262
+ *
263
+ * async function* rows() {
264
+ * yield "order_id,total\n";
265
+ * for await (var r of cursor) yield r.id + "," + r.total + "\n";
266
+ * }
267
+ *
268
+ * // Taking the signal lets the query itself be cancelled when the client
269
+ * // hangs up, instead of running to completion against a closed socket.
270
+ * await b.render.stream(res, function (signal) { return rows(signal); }, {
271
+ * headers: { "Content-Type": "text/csv; charset=utf-8" },
272
+ * });
273
+ */
274
+ async function stream(res, iterable, opts) {
275
+ opts = opts || {};
276
+ validateOpts(opts, ["status", "headers", "onError", "signal"], "render.stream");
277
+ if (opts.onError !== undefined && opts.onError !== "destroy" && opts.onError !== "rethrow") {
278
+ throw new TypeError("render.stream: opts.onError must be \"destroy\" or \"rethrow\"");
279
+ }
280
+ if (_alreadyDone(res)) return;
281
+
282
+ // Read the caller's headers FIRST, while there is nothing to release. Copying
283
+ // them runs whatever `opts.headers` chooses to run — a getter, a proxy trap —
284
+ // and doing it after the producer was opened left a throw there with no path
285
+ // back to the cleanup, holding the producer's cursor or file handle for the
286
+ // life of the process. Here it is simply a bad argument, and reaches the
287
+ // caller as one.
288
+ var headers = _mergedHeaders(_defaultContentTypeUnlessStated(res, {
289
+ "Content-Type": "application/octet-stream",
290
+ "Cache-Control": DEFAULT_DYNAMIC_CACHE_CONTROL,
291
+ }), opts.headers);
292
+
293
+ // Stopping has to reach the producer, not only the loop. An async generator
294
+ // suspended inside its own `await` does not run its `finally` when `return()`
295
+ // is called: that call queues behind the very pull it is trying to cancel, so
296
+ // a cursor stays open until a query nobody is waiting for finishes on its
297
+ // own. Handing the producer a signal is the only way to reach work already in
298
+ // flight, so a producer that takes one is given this one.
299
+ var stopper = new AbortController();
300
+ var unlink = _linkSignal(opts.signal, stopper);
301
+
302
+ // The source is opened before anything is committed. A factory that throws,
303
+ // or an object whose `Symbol.asyncIterator` throws, then reaches the caller's
304
+ // error page normally — with the status line still unsent, rather than as a
305
+ // committed response with no body to follow.
306
+ // "Before the status line goes out" is this primitive's own status line. The
307
+ // CALLER may have sent one already — the documented shape where a route
308
+ // writes its own head and streams into it, which is how SSE is written — and
309
+ // then there is no pre-commit window to fail into: those headers are on the
310
+ // wire and the only honest ending left is an incomplete transfer. Every throw
311
+ // out of the opening sequence goes through here so that a response someone
312
+ // else committed is never left neither ended nor destroyed, which reads to
313
+ // the client as a body that never arrives.
314
+ function failBeforeStreaming(e, openedSource) {
315
+ if (openedSource) _stopProducer(stopper, openedSource, unlink);
316
+ else unlink();
317
+ if (res.headersSent === true && opts.onError !== "rethrow") {
318
+ requestHelpers().failAfterHeaders(res);
319
+ }
320
+ throw e;
321
+ }
322
+
323
+ var source;
324
+ try {
325
+ source = _openSource(iterable, stopper.signal);
326
+ } catch (e) {
327
+ failBeforeStreaming(e, null);
328
+ }
329
+
330
+ // Opening an async generator does no work — its body does not run until the
331
+ // first pull — so "the source opened" said nothing about whether the producer
332
+ // can produce, which is the source type this primitive is built around. The
333
+ // first value is fetched HERE, while the status line is still unsent, so a
334
+ // query that fails before any row reaches the caller's error page instead of
335
+ // arriving as a download that dies partway. That is the failure most likely
336
+ // to be seen in production, and the only one an error page can explain.
337
+ //
338
+ // A stop that arrives during the first pull is NOT a pre-commit failure: an
339
+ // aborted export is a truncation whether or not a row was ever produced, and
340
+ // it takes the committed path below so the client hears an incomplete
341
+ // transfer rather than a successful empty one.
342
+ // Only where THIS call is the one that will commit. A route that sent its own
343
+ // status line has no pre-commit window to protect, and waiting for a first
344
+ // value there buys nothing while costing everything: an event stream whose
345
+ // first event is minutes away would hold its headers back for minutes, and
346
+ // the client would see a connection that never established. That shape — send
347
+ // the head, flush it, then stream — is also the escape hatch for a long-lived
348
+ // producer that wants its headers out before it has anything to say.
349
+ var closed = _closedSignal(res);
350
+ var stopped = null; // "abort" | "peer" | null
351
+ var pending = null; // the first step, held back
352
+ var willCommitHere = res.headersSent !== true && typeof res.writeHead === "function";
353
+ if (!willCommitHere) pending = null;
354
+ else if (stopper.signal.aborted) stopped = "abort";
355
+ else if (_peerGone(res)) stopped = "peer";
356
+ else {
357
+ try {
358
+ // Until the producer offers something that would actually be WRITTEN, or
359
+ // says it is done. A `null` or `undefined` is skipped by the loop below
360
+ // rather than written, so it is not proof the producer can produce — a
361
+ // source that yields one and then fails would otherwise have committed
362
+ // the response on the strength of a value nobody ever sends.
363
+ for (;;) {
364
+ pending = _requireIteratorResult(
365
+ await _raceStop(Promise.resolve(source.next()), stopper.signal, closed));
366
+ if (pending.done) break;
367
+ // A SYNC iterable may yield a PROMISE, and `for await` resolves it
368
+ // before handing it on — so a rejected one is another failure before
369
+ // any chunk was produced, and it belongs in this window rather than in
370
+ // the loop. Resolved here, the loop sees a settled value and writes it.
371
+ if (!source.isAsync && pending.value && typeof pending.value.then === "function") {
372
+ pending = {
373
+ done: false,
374
+ value: await _raceStop(pending.value, stopper.signal, closed),
375
+ };
376
+ }
377
+ if (pending.value !== null && pending.value !== undefined) break;
378
+ }
379
+ } catch (e) {
380
+ if (e && e.stopKind) stopped = e.stopKind;
381
+ else {
382
+ closed.dispose();
383
+ failBeforeStreaming(e, source);
384
+ }
385
+ }
386
+ }
387
+
388
+ if (res.headersSent !== true && typeof res.writeHead === "function") {
389
+ try {
390
+ res.writeHead(_statusOr(opts, C.HTTP.STATUS.OK), headers);
391
+ } catch (e) {
392
+ // A status out of range or a header value Node refuses. The producer is
393
+ // already open by now, so a configuration mistake would otherwise leave
394
+ // its cursor or file handle held for the life of the process. A partial
395
+ // writeHead can also leave the response committed, which is why this
396
+ // takes the same route out as every other failure before the loop.
397
+ closed.dispose();
398
+ failBeforeStreaming(e, source);
399
+ }
400
+ }
401
+
402
+ // The status line went out above, so from here the response is committed
403
+ // whether or not a chunk has been written yet. Treating "committed" as "at
404
+ // least one chunk landed" left a producer that failed on its first row with
405
+ // a response that was neither ended nor destroyed — headers and
406
+ // `Transfer-Encoding: chunked` on the wire and nothing to follow, so the
407
+ // client waited forever.
408
+ // Three things can stop this loop and each has to release the producer: the
409
+ // producer failing, the caller aborting, and the peer going away. A
410
+ // `for await` releases the iterator only when IT decides the loop is over,
411
+ // so a pull that never settles — a query that hangs, a page that never
412
+ // arrives — kept the handler, the cursor and the response alive with no way
413
+ // out. The loop owns its own stopping instead.
414
+ try {
415
+ for (;;) {
416
+ if (stopped !== null) break; // decided before the commit
417
+ if (stopper.signal.aborted) { stopped = "abort"; break; }
418
+ // The peer is gone: stop pulling rows nobody will read, and do not wait
419
+ // for a drain that never comes.
420
+ if (_peerGone(res)) { stopped = "peer"; break; }
421
+ var step;
422
+ if (pending !== null) { step = pending; pending = null; } // the pre-commit pull
423
+ else {
424
+ try {
425
+ step = _requireIteratorResult(
426
+ await _raceStop(Promise.resolve(source.next()), stopper.signal, closed));
427
+ } catch (e) {
428
+ if (e && e.stopKind) { stopped = e.stopKind; break; }
429
+ throw e;
430
+ }
431
+ }
432
+ if (step.done) break;
433
+ var chunk = step.value;
434
+ try {
435
+ // A synchronous iterable may still yield promises, and `for await`
436
+ // resolves each one before handing it on. Writing the promise object
437
+ // instead would put "[object Promise]" in the export.
438
+ if (!source.isAsync && chunk && typeof chunk.then === "function") {
439
+ chunk = await _raceStop(chunk, stopper.signal, closed);
440
+ }
441
+ if (chunk === null || chunk === undefined) continue;
442
+ await _raceStop(safeAsync().writeChunk(res, chunk), stopper.signal, closed);
443
+ } catch (e2) {
444
+ if (e2 && e2.stopKind) { stopped = e2.stopKind; break; }
445
+ throw e2;
446
+ }
447
+ }
448
+ } catch (e) {
449
+ _stopProducer(stopper, source, unlink);
450
+ closed.dispose();
451
+ if (opts.onError !== "rethrow") requestHelpers().failAfterHeaders(res);
452
+ throw e;
453
+ }
454
+ // Only stop a producer the loop LEFT early. A `for await` calls `return()` on
455
+ // an early exit and not on normal exhaustion, and producers use it to record
456
+ // exactly that — telling one its completed export was cancelled is a
457
+ // different statement.
458
+ if (stopped !== null) _stopProducer(stopper, source, unlink);
459
+ else unlink();
460
+ closed.dispose();
461
+ // An abort is a truncation too. Ending normally would write the terminating
462
+ // chunk and hand the client four rows of a fifty-row export as a complete,
463
+ // successful 200 — the very outcome this primitive exists to prevent, and
464
+ // the caller would not hear about it either. A signal that was already
465
+ // aborted when the call began takes the same path: an empty export is still
466
+ // a truncated one.
467
+ if (stopped !== null) {
468
+ requestHelpers().failAfterHeaders(res);
469
+ return;
470
+ }
471
+ if (typeof res.end === "function" && !_alreadyDone(res)) res.end();
472
+ }
473
+
474
+ // Open the producer. A function is called with the stop signal, so a producer
475
+ // whose work is cancellable can be handed the one thing that reaches work
476
+ // already in flight; anything else is used as the iterable it claims to be.
477
+ function _openSource(iterable, signal) {
478
+ var it = typeof iterable === "function" ? iterable(signal) : iterable;
479
+ var isAsync = !!(it && typeof it[Symbol.asyncIterator] === "function");
480
+ if (!it || (!isAsync && typeof it[Symbol.iterator] !== "function")) {
481
+ throw new TypeError("render.stream: expected an async or sync iterable of chunks, " +
482
+ "or a function returning one");
483
+ }
484
+ var iterator = isAsync ? it[Symbol.asyncIterator]() : it[Symbol.iterator]();
485
+ // What the method HANDED BACK has to be an iterator, and that is asked here
486
+ // rather than at the first pull. Left until then, a method returning `null`,
487
+ // a number, or an object with no `next` came to light after the status line
488
+ // had gone out — so a malformed producer arrived as a committed response that
489
+ // was then destroyed, instead of as a bad argument the caller could still
490
+ // render an error page for.
491
+ if (!iterator || (typeof iterator !== "object" && typeof iterator !== "function") ||
492
+ typeof iterator.next !== "function") {
493
+ throw new TypeError("render.stream: the iterable's " +
494
+ (isAsync ? "Symbol.asyncIterator" : "Symbol.iterator") +
495
+ " returned something that is not an iterator — it must return an object " +
496
+ "with a next() method");
497
+ }
498
+ return {
499
+ isAsync: isAsync,
500
+ next: function () { return iterator.next(); },
501
+ "return": function () {
502
+ return typeof iterator["return"] === "function" ? iterator["return"]() : undefined;
503
+ },
504
+ };
505
+ }
506
+
507
+ // The iterator protocol says a step is an object. Treating anything else as "no
508
+ // chunk this time" would pull again from a producer that can only answer the
509
+ // same way, and spin forever with the response held open; `for await` raises
510
+ // here and so does this. Asked of the FIRST step too, which is before the status
511
+ // line goes out, so a producer that answers wrongly from the start is a bad
512
+ // argument rather than a committed response.
513
+ function _requireIteratorResult(step) {
514
+ if (!step || (typeof step !== "object" && typeof step !== "function")) {
515
+ throw new TypeError("render.stream: the producer returned " +
516
+ (step === undefined ? "undefined" : JSON.stringify(step)) +
517
+ " where an iterator result was expected");
518
+ }
519
+ return step;
520
+ }
521
+
522
+ // Stop the producer, signal first. `return()` alone is not enough: on an async
523
+ // generator it queues behind the pull it means to cancel, so a generator parked
524
+ // in `await query()` does not reach its `finally` — and the cursor, file handle
525
+ // or connection it holds stays open — until that query finishes on its own. The
526
+ // signal reaches the pending work; `return()` then unwinds the generator.
527
+ //
528
+ // Neither is awaited. A producer blocked on the same thing that stalled the
529
+ // pull would otherwise block the response's own teardown behind it.
530
+ function _stopProducer(stopper, source, unlink) {
531
+ unlink();
532
+ try { stopper.abort(); } catch (_a) { /* already aborted */ }
533
+ if (!source) return;
534
+ try {
535
+ var maybe = source["return"]();
536
+ if (maybe && typeof maybe.then === "function") maybe.then(_ignore, _ignore);
537
+ } catch (_e) { /* the producer is entitled to refuse */ }
538
+ }
539
+
540
+ // Forward the caller's abort to the stop signal, and hand back the undo. A
541
+ // per-request signal outlives a single export, so the listener is removed when
542
+ // the stream is over rather than left to accumulate one per response.
543
+ function _linkSignal(signal, stopper) {
544
+ if (!signal) return _ignore;
545
+ if (signal.aborted) {
546
+ stopper.abort();
547
+ return _ignore;
548
+ }
549
+ function onAbort() { stopper.abort(); }
550
+ signal.addEventListener("abort", onAbort, { once: true });
551
+ return function () { signal.removeEventListener("abort", onAbort); };
552
+ }
553
+
554
+ function _ignore() {}
555
+
556
+ // Take delivery of a promise whose outcome no longer matters, so that a
557
+ // rejection nobody is waiting for does not surface as an unhandled one.
558
+ function _observe(promise) {
559
+ if (promise && typeof promise.then === "function") promise.then(_ignore, _ignore);
560
+ }
561
+
562
+ // A promise that settles when the response closes, so a pull blocked on the
563
+ // next database page does not outlive the client that asked for it. The
564
+ // listener is removed either way — a long-lived response would otherwise
565
+ // accumulate one per stream.
566
+ function _closedSignal(res) {
567
+ var fired = _peerGone(res);
568
+ // A subscriber list rather than a promise. Racing a promise would attach one
569
+ // reaction per chunk to the same unresolved promise, and a settled race
570
+ // cannot detach it — so a long export accumulated a closure for every row it
571
+ // wrote, and the memory it was careful not to spend on buffering went here
572
+ // instead.
573
+ var waiting = [];
574
+ function onClose() {
575
+ fired = true;
576
+ var pending = waiting;
577
+ waiting = [];
578
+ for (var i = 0; i < pending.length; i += 1) pending[i]();
579
+ }
580
+ if (res && typeof res.once === "function") res.once("close", onClose);
581
+ return {
582
+ isClosed: function () { return fired || _peerGone(res); },
583
+ subscribe: function (fn) {
584
+ if (fired) { fn(); return function () {}; }
585
+ waiting.push(fn);
586
+ return function () {
587
+ var at = waiting.indexOf(fn);
588
+ if (at !== -1) waiting.splice(at, 1);
589
+ };
590
+ },
591
+ dispose: function () {
592
+ waiting = [];
593
+ if (res && typeof res.removeListener === "function") res.removeListener("close", onClose);
594
+ },
595
+ };
596
+ }
597
+
598
+ // Settle as soon as the caller aborts or the peer closes, so neither a blocked
599
+ // producer nor a wait for `drain` from a peer that stopped reading outlives the
600
+ // reason to keep going.
601
+ function _raceStop(promise, signal, closed) {
602
+ if (!signal && !closed) return promise;
603
+ // Already stopped before the race could be set up — but the pull is in flight
604
+ // whatever we decide about it. Left unobserved, its later rejection is an
605
+ // unhandled rejection, which by default takes the process down: a producer
606
+ // that cancels itself and then lets its query reject would kill the server.
607
+ if (signal && signal.aborted) {
608
+ _observe(promise);
609
+ return Promise.reject(_stopError("abort"));
610
+ }
611
+ if (closed && closed.isClosed()) {
612
+ _observe(promise);
613
+ return Promise.reject(_stopError("peer"));
614
+ }
615
+ return new Promise(function (resolve, reject) {
616
+ var settled = false;
617
+ var unsubscribe = null;
618
+ function done(fn, v) {
619
+ if (settled) return;
620
+ settled = true;
621
+ if (signal) signal.removeEventListener("abort", onAbort);
622
+ if (unsubscribe) unsubscribe();
623
+ fn(v);
624
+ }
625
+ function onAbort() { done(reject, _stopError("abort")); }
626
+ if (signal) signal.addEventListener("abort", onAbort, { once: true });
627
+ if (closed) unsubscribe = closed.subscribe(function () { done(reject, _stopError("peer")); });
628
+ promise.then(function (v) { done(resolve, v); }, function (e) { done(reject, e); });
629
+ });
630
+ }
631
+
632
+ function _stopError(kind) {
633
+ var err = new Error("render.stream: " + (kind === "abort" ? "aborted" : "the peer closed"));
634
+ err.code = kind === "abort" ? "render/aborted" : "render/peer-closed";
635
+ err.stopKind = kind;
636
+ return err;
637
+ }
638
+
639
+ // A response whose socket has been destroyed or whose peer has hung up. Both
640
+ // are reported differently across Node versions and response doubles, so all
641
+ // three signals are consulted.
642
+ function _peerGone(res) {
643
+ if (!res) return true;
644
+ if (res.destroyed === true || res.writableEnded === true) return true;
645
+ return !!(res.socket && res.socket.destroyed === true);
124
646
  }
125
647
 
126
648
  /**
@@ -153,7 +675,7 @@ function text(res, body, opts) {
153
675
  "Content-Length": Buffer.byteLength(encoded, charset),
154
676
  "Cache-Control": DEFAULT_DYNAMIC_CACHE_CONTROL,
155
677
  }, opts.headers);
156
- _writeResponse(res, opts.status || 200, headers, encoded);
678
+ _writeResponse(res, _statusOr(opts, C.HTTP.STATUS.OK), headers, encoded);
157
679
  }
158
680
 
159
681
  /**
@@ -187,7 +709,7 @@ function htmlString(res, htmlBody, opts) {
187
709
  "Content-Length": Buffer.byteLength(encoded, charset),
188
710
  "Cache-Control": DEFAULT_DYNAMIC_CACHE_CONTROL,
189
711
  }, opts.headers);
190
- _writeResponse(res, opts.status || 200, headers, encoded);
712
+ _writeResponse(res, _statusOr(opts, C.HTTP.STATUS.OK), headers, encoded);
191
713
  }
192
714
 
193
715
  /**
@@ -217,8 +739,8 @@ function redirect(res, location, opts) {
217
739
  if (typeof location !== "string" || location.length === 0) {
218
740
  throw new Error("render.redirect: location is required");
219
741
  }
220
- var status = opts.status || 302;
221
- if (status < 300 || status > 399) {
742
+ var status = _statusOr(opts, C.HTTP.STATUS.FOUND);
743
+ if (!C.HTTP.redirect(status)) {
222
744
  throw new Error("render.redirect: status must be 3xx (got " + status + ")");
223
745
  }
224
746
  var headers = _mergedHeaders({
@@ -292,6 +814,7 @@ function create(opts) {
292
814
  html: html,
293
815
  htmlString: htmlString,
294
816
  json: json,
817
+ stream: stream,
295
818
  text: text,
296
819
  redirect: redirect,
297
820
  engine: engine,
@@ -301,6 +824,7 @@ function create(opts) {
301
824
  module.exports = {
302
825
  create: create,
303
826
  json: json,
827
+ stream: stream,
304
828
  text: text,
305
829
  htmlString: htmlString,
306
830
  redirect: redirect,