@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.
- package/CHANGELOG.md +4 -0
- package/README.md +1 -0
- package/SECURITY.md +17 -0
- package/lib/addresses.js +2 -2
- package/lib/admin.js +49 -45
- package/lib/affiliates.js +7 -23
- package/lib/announcement-bar.js +5 -22
- package/lib/api-keys.js +4 -13
- package/lib/asset-manifest.json +1 -1
- package/lib/banner-ab-tests.js +5 -9
- package/lib/blog-articles.js +8 -26
- package/lib/captcha-gate.js +7 -17
- package/lib/carrier-accounts.js +3 -10
- package/lib/catalog-drafts.js +5 -12
- package/lib/category-navigation.js +4 -20
- package/lib/cms-blocks.js +4 -7
- package/lib/compliance-export.js +4 -11
- package/lib/consent-ledger.js +2 -4
- package/lib/cost-layers.js +56 -21
- package/lib/customer-import.js +2 -2
- package/lib/customer-notes.js +7 -23
- package/lib/customer-roles.js +3 -5
- package/lib/customer-segments.js +17 -14
- package/lib/customer-surveys.js +7 -14
- package/lib/customers.js +2 -4
- package/lib/dispute-resolution.js +5 -13
- package/lib/dropship-forwarding.js +3 -5
- package/lib/dunning.js +2 -7
- package/lib/email-ab-tests.js +4 -8
- package/lib/email-campaigns.js +25 -14
- package/lib/email-warmup.js +2 -7
- package/lib/experiments.js +6 -10
- package/lib/externaldb-d1.js +78 -44
- package/lib/geolocation.js +18 -28
- package/lib/gift-options.js +5 -23
- package/lib/knowledge-base.js +7 -22
- package/lib/line-gift-wrap.js +3 -17
- package/lib/live-chat.js +5 -18
- package/lib/metered-usage.js +3 -8
- package/lib/notifications.js +3 -7
- package/lib/operator-accounts.js +3 -5
- package/lib/operator-approvals.js +6 -8
- package/lib/operator-audit-log.js +2 -7
- package/lib/operator-help-center.js +5 -21
- package/lib/operator-inbox.js +4 -11
- package/lib/operator-roles.js +5 -7
- package/lib/order-escalation.js +2 -2
- package/lib/order-export.js +24 -20
- package/lib/order-notes.js +4 -14
- package/lib/order-ratings.js +4 -10
- package/lib/payment-methods.js +5 -13
- package/lib/payment-retries.js +2 -7
- package/lib/payment.js +103 -58
- package/lib/pixel-events.js +8 -23
- package/lib/plan-changes.js +2 -7
- package/lib/product-qa.js +4 -8
- package/lib/promo-banners.js +7 -15
- package/lib/purchase-orders.js +3 -6
- package/lib/push-notifications.js +7 -32
- package/lib/pwa-manifest.js +6 -11
- package/lib/quotes.js +5 -9
- package/lib/referrals.js +10 -6
- package/lib/reviews.js +4 -10
- package/lib/robots-config.js +6 -14
- package/lib/sales-tax-filings.js +2 -2
- package/lib/search-facets.js +5 -5
- package/lib/search-ranking.js +4 -7
- package/lib/search-synonyms.js +7 -21
- package/lib/security-middleware.js +125 -70
- package/lib/seller-signup.js +10 -26
- package/lib/sidebar-widgets.js +6 -15
- package/lib/site-redirects.js +49 -137
- package/lib/sitemap-generator.js +3 -7
- package/lib/sms-dispatcher.js +7 -23
- package/lib/stock-receipts.js +4 -10
- package/lib/storefront-dashboards.js +2 -10
- package/lib/storefront-forms.js +9 -22
- package/lib/storefront-pages.js +6 -20
- package/lib/storefront.js +21 -23
- package/lib/subscription-billing.js +3 -8
- package/lib/subscription-controls.js +2 -7
- package/lib/suggestion-box.js +9 -26
- package/lib/support-tickets.js +6 -24
- package/lib/tax-remittance.js +2 -2
- package/lib/tenants.js +2 -7
- package/lib/text-guard.js +137 -35
- package/lib/theme-assets.js +4 -16
- package/lib/translations.js +3 -6
- package/lib/trust-badges.js +4 -7
- package/lib/vendor/MANIFEST.json +107 -97
- package/lib/vendor/blamejs/CHANGELOG.md +180 -0
- package/lib/vendor/blamejs/NOTICE +14 -10
- package/lib/vendor/blamejs/README.md +17 -5
- package/lib/vendor/blamejs/index.js +2 -0
- package/lib/vendor/blamejs/lib/acme.js +9 -9
- package/lib/vendor/blamejs/lib/api-snapshot.js +35 -16
- package/lib/vendor/blamejs/lib/app-shutdown.js +1 -1
- package/lib/vendor/blamejs/lib/archive-adapters.js +1 -1
- package/lib/vendor/blamejs/lib/archive.js +13 -24
- package/lib/vendor/blamejs/lib/auth/access-lock.js +2 -1
- package/lib/vendor/blamejs/lib/auth/bot-challenge.js +3 -6
- package/lib/vendor/blamejs/lib/auth/ciba.js +32 -3
- package/lib/vendor/blamejs/lib/auth/oauth.js +65 -8
- package/lib/vendor/blamejs/lib/auth/password.js +1 -1
- package/lib/vendor/blamejs/lib/auth/saml.js +21 -3
- package/lib/vendor/blamejs/lib/auth-bot-challenge.js +3 -3
- package/lib/vendor/blamejs/lib/backup/index.js +5 -5
- package/lib/vendor/blamejs/lib/case-fold-classes.js +491 -0
- package/lib/vendor/blamejs/lib/codepoint-class.js +131 -0
- package/lib/vendor/blamejs/lib/constants.js +79 -0
- package/lib/vendor/blamejs/lib/db-declare-view.js +8 -2
- package/lib/vendor/blamejs/lib/error-page.js +20 -1
- package/lib/vendor/blamejs/lib/external-db.js +5 -1
- package/lib/vendor/blamejs/lib/flag-evaluation-context.js +24 -6
- package/lib/vendor/blamejs/lib/flag.js +28 -2
- package/lib/vendor/blamejs/lib/framework-schema.js +14 -3
- package/lib/vendor/blamejs/lib/guard-regex.js +2345 -89
- package/lib/vendor/blamejs/lib/http-client.js +217 -40
- package/lib/vendor/blamejs/lib/mail-bimi.js +1 -1
- package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +3 -2
- package/lib/vendor/blamejs/lib/mail-dav.js +26 -26
- package/lib/vendor/blamejs/lib/mail-deploy.js +10 -10
- package/lib/vendor/blamejs/lib/mail-scan.js +3 -0
- package/lib/vendor/blamejs/lib/mail-store.js +7 -1
- package/lib/vendor/blamejs/lib/mail.js +5 -1
- package/lib/vendor/blamejs/lib/middleware/age-gate.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +1 -1
- package/lib/vendor/blamejs/lib/middleware/assetlinks.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/bearer-auth.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/compression.js +1 -1
- package/lib/vendor/blamejs/lib/middleware/cors.js +1 -1
- package/lib/vendor/blamejs/lib/middleware/csp-report.js +5 -5
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/daily-byte-quota.js +2 -2
- package/lib/vendor/blamejs/lib/middleware/deny-response.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/dpop.js +2 -2
- package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/flag-context.js +17 -0
- package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +10 -8
- package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/require-aal.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/require-auth.js +4 -3
- package/lib/vendor/blamejs/lib/middleware/require-bound-key.js +4 -3
- package/lib/vendor/blamejs/lib/middleware/require-content-type.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/require-methods.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/require-mtls.js +3 -2
- package/lib/vendor/blamejs/lib/middleware/security-txt.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/web-app-manifest.js +2 -1
- package/lib/vendor/blamejs/lib/network-dns-resolver.js +1 -1
- package/lib/vendor/blamejs/lib/network-dns.js +3 -3
- package/lib/vendor/blamejs/lib/network-nts.js +35 -14
- package/lib/vendor/blamejs/lib/network-proxy.js +42 -4
- package/lib/vendor/blamejs/lib/network-smtp-policy.js +1 -1
- package/lib/vendor/blamejs/lib/network-tls.js +341 -11
- package/lib/vendor/blamejs/lib/nist-crosswalk.js +2 -2
- package/lib/vendor/blamejs/lib/ntp-check.js +30 -19
- package/lib/vendor/blamejs/lib/object-store/azure-blob.js +1 -1
- package/lib/vendor/blamejs/lib/object-store/gcs.js +1 -1
- package/lib/vendor/blamejs/lib/object-store/http-put.js +2 -1
- package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +5 -5
- package/lib/vendor/blamejs/lib/object-store/sigv4.js +2 -2
- package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +4 -2
- package/lib/vendor/blamejs/lib/openapi.js +1 -1
- package/lib/vendor/blamejs/lib/pqc-agent.js +4 -7
- package/lib/vendor/blamejs/lib/problem-details.js +2 -1
- package/lib/vendor/blamejs/lib/redis-client.js +12 -1
- package/lib/vendor/blamejs/lib/regex-linear.js +1349 -0
- package/lib/vendor/blamejs/lib/render.js +531 -7
- package/lib/vendor/blamejs/lib/request-helpers.js +181 -5
- package/lib/vendor/blamejs/lib/router.js +37 -13
- package/lib/vendor/blamejs/lib/safe-async.js +142 -0
- package/lib/vendor/blamejs/lib/safe-icap.js +3 -0
- package/lib/vendor/blamejs/lib/safe-sql.js +15 -3
- package/lib/vendor/blamejs/lib/self-update.js +2 -2
- package/lib/vendor/blamejs/lib/testing.js +54 -7
- package/lib/vendor/blamejs/lib/validate-opts.js +56 -0
- package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +62 -25
- package/lib/vendor/blamejs/lib/vendor/blamejs-pki.cjs +2004 -142
- package/lib/vendor/blamejs/lib/vendor/browser/noble-ciphers.mjs +727 -0
- package/lib/vendor/blamejs/lib/vendor/browser/noble-hashes.mjs +1070 -0
- package/lib/vendor/blamejs/lib/vendor/browser/noble-post-quantum.mjs +1090 -0
- package/lib/vendor/blamejs/lib/vendor/noble-post-quantum.cjs +510 -227
- package/lib/vendor/blamejs/lib/webhook-hmac.js +19 -2
- package/lib/vendor/blamejs/lib/webhook.js +1 -1
- package/lib/vendor/blamejs/lib/ws-client.js +24 -2
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/sbom.cdx.json +6 -6
- package/lib/vendor-invoices.js +3 -6
- package/lib/vendors.js +5 -19
- package/lib/webhook-receiver.js +46 -83
- package/lib/webhook-subscriptions.js +3 -13
- package/lib/wishlist-sharing.js +5 -10
- 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:
|
|
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
|
-
|
|
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),
|
|
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
|
|
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:
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
1175
|
-
res
|
|
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
|
-
|
|
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 ===
|
|
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:
|
|
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", {
|