@blamejs/blamejs-shop 0.3.42 → 0.3.44
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/lib/admin.js +641 -1
- package/lib/asset-manifest.json +1 -1
- package/lib/order-ratings.js +36 -0
- package/lib/storefront.js +253 -0
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +6 -2
- package/lib/vendor/blamejs/lib/app.js +8 -0
- package/lib/vendor/blamejs/lib/mail.js +1 -0
- package/lib/vendor/blamejs/lib/network-dns.js +1 -0
- package/lib/vendor/blamejs/lib/network-nts.js +4 -0
- package/lib/vendor/blamejs/lib/ntp-check.js +8 -0
- package/lib/vendor/blamejs/lib/redis-client.js +8 -0
- package/lib/vendor/blamejs/lib/validate-opts.js +23 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.16.json +45 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +29 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/validate-opts-port.test.js +70 -0
- package/package.json +1 -1
package/lib/asset-manifest.json
CHANGED
package/lib/order-ratings.js
CHANGED
|
@@ -455,6 +455,41 @@ function create(opts) {
|
|
|
455
455
|
return _topRatings(input, "topNegativeRatings", "negative");
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
+
// ---- listFlagged -----------------------------------------------------
|
|
459
|
+
|
|
460
|
+
// The moderation queue: every rating whose comment an operator has
|
|
461
|
+
// flagged, most-recent-first, backed by the (comment_flagged,
|
|
462
|
+
// occurred_at) index. Unlike topNegativeRatings (score-ordered, capped
|
|
463
|
+
// at MAX_TOP_LIMIT), this surfaces ALL flagged comments regardless of
|
|
464
|
+
// their score — so a flagged comment on an otherwise high rating is
|
|
465
|
+
// never hidden behind the score cap. The from/to window is optional and
|
|
466
|
+
// applied only when supplied; the row count is capped by `limit`.
|
|
467
|
+
async function listFlagged(input) {
|
|
468
|
+
input = (input && typeof input === "object") ? input : {};
|
|
469
|
+
var limit = _limit(input.limit, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT, "limit");
|
|
470
|
+
|
|
471
|
+
var sql = "SELECT * FROM order_ratings WHERE comment_flagged = 1";
|
|
472
|
+
var params = [];
|
|
473
|
+
if (input.from != null || input.to != null) {
|
|
474
|
+
var from = _epoch(input.from, "from");
|
|
475
|
+
var to = _epoch(input.to, "to");
|
|
476
|
+
if (from > to) {
|
|
477
|
+
throw new TypeError("orderRatings.listFlagged: from must be <= to");
|
|
478
|
+
}
|
|
479
|
+
params.push(from);
|
|
480
|
+
sql += " AND occurred_at >= ?" + params.length;
|
|
481
|
+
params.push(to);
|
|
482
|
+
sql += " AND occurred_at <= ?" + params.length;
|
|
483
|
+
}
|
|
484
|
+
params.push(limit);
|
|
485
|
+
sql += " ORDER BY occurred_at DESC, id DESC LIMIT ?" + params.length;
|
|
486
|
+
|
|
487
|
+
var r = await query(sql, params);
|
|
488
|
+
var out = [];
|
|
489
|
+
for (var i = 0; i < r.rows.length; i += 1) out.push(_decode(r.rows[i]));
|
|
490
|
+
return out;
|
|
491
|
+
}
|
|
492
|
+
|
|
458
493
|
return {
|
|
459
494
|
RATING_AXES: RATING_AXES.slice(),
|
|
460
495
|
MIN_RATING: MIN_RATING,
|
|
@@ -471,6 +506,7 @@ function create(opts) {
|
|
|
471
506
|
responseToCustomer: responseToCustomer,
|
|
472
507
|
topPositiveRatings: topPositiveRatings,
|
|
473
508
|
topNegativeRatings: topNegativeRatings,
|
|
509
|
+
listFlagged: listFlagged,
|
|
474
510
|
};
|
|
475
511
|
}
|
|
476
512
|
|
package/lib/storefront.js
CHANGED
|
@@ -6128,6 +6128,7 @@ var ORDER_PAGE =
|
|
|
6128
6128
|
" </table>\n" +
|
|
6129
6129
|
" </div>\n" +
|
|
6130
6130
|
" RAW_ORDER_ACTIONS" +
|
|
6131
|
+
" RAW_ORDER_RATING" +
|
|
6131
6132
|
" </div>\n" +
|
|
6132
6133
|
" <aside class=\"order-page__totals\">\n" +
|
|
6133
6134
|
" RAW_ORDER_TIMELINE" +
|
|
@@ -6335,6 +6336,134 @@ function _orderActionsBlock(o) {
|
|
|
6335
6336
|
return "<div class=\"order-page__actions\">" + btns.join("") + "</div>";
|
|
6336
6337
|
}
|
|
6337
6338
|
|
|
6339
|
+
// A post-purchase rating is offered on the same window a return is: a paid
|
|
6340
|
+
// (or further-along) order, never a still-pending (unpaid) one nor the
|
|
6341
|
+
// terminal off-ramps (cancelled / refunded). The order FSM has no
|
|
6342
|
+
// `fulfilled` status — the post-payment states are paid / fulfilling /
|
|
6343
|
+
// shipped / delivered — so the rating window mirrors _orderEligibleForReturn
|
|
6344
|
+
// rather than gating on a status the primitive doesn't expose.
|
|
6345
|
+
function _orderEligibleForRating(status) {
|
|
6346
|
+
return status === "paid" || status === "fulfilling" ||
|
|
6347
|
+
status === "shipped" || status === "delivered";
|
|
6348
|
+
}
|
|
6349
|
+
|
|
6350
|
+
// The three rating axes the customer scores, in display order. Labels are
|
|
6351
|
+
// static framework copy; the axis key matches the submitRating field name.
|
|
6352
|
+
var ORDER_RATING_AXES = [
|
|
6353
|
+
{ key: "shipping", label: "Shipping", field: "shipping_rating" },
|
|
6354
|
+
{ key: "packaging", label: "Packaging", field: "packaging_rating" },
|
|
6355
|
+
{ key: "recommend", label: "Recommend us", field: "recommend_rating" },
|
|
6356
|
+
];
|
|
6357
|
+
|
|
6358
|
+
// 1–5 selector for one rating axis. Rendered as native radio buttons so the
|
|
6359
|
+
// form works with no JavaScript and a screen reader announces each option.
|
|
6360
|
+
// `field` is the submitRating field name; nothing here is operator/customer
|
|
6361
|
+
// input, but the labels are escaped at the sink for consistency.
|
|
6362
|
+
function _ratingAxisField(axis) {
|
|
6363
|
+
var esc = b.template.escapeHtml;
|
|
6364
|
+
var opts = "";
|
|
6365
|
+
for (var v = 1; v <= 5; v += 1) {
|
|
6366
|
+
opts +=
|
|
6367
|
+
"<label class=\"order-rating__option\">" +
|
|
6368
|
+
"<input type=\"radio\" name=\"" + esc(axis.field) + "\" value=\"" + v + "\" required>" +
|
|
6369
|
+
"<span>" + v + "</span>" +
|
|
6370
|
+
"</label>";
|
|
6371
|
+
}
|
|
6372
|
+
return "<fieldset class=\"order-rating__axis\">" +
|
|
6373
|
+
"<legend>" + esc(axis.label) + "</legend>" +
|
|
6374
|
+
"<div class=\"order-rating__scale\">" + opts + "</div>" +
|
|
6375
|
+
"</fieldset>";
|
|
6376
|
+
}
|
|
6377
|
+
|
|
6378
|
+
// The rating form (eligible, not-yet-rated order). Posts to
|
|
6379
|
+
// /orders/:id/rate; the container CSRF injection tokens it automatically
|
|
6380
|
+
// (the action is not an EDGE_POST_PATHS prefix). A correction notice from a
|
|
6381
|
+
// rejected submit (bad value / over-length comment / duplicate) renders
|
|
6382
|
+
// above the form.
|
|
6383
|
+
function _orderRatingForm(o, notice) {
|
|
6384
|
+
var esc = b.template.escapeHtml;
|
|
6385
|
+
var noticeHtml = notice
|
|
6386
|
+
? "<p class=\"form-notice form-notice--err\" role=\"alert\">" + esc(String(notice)) + "</p>"
|
|
6387
|
+
: "";
|
|
6388
|
+
var axes = ORDER_RATING_AXES.map(_ratingAxisField).join("");
|
|
6389
|
+
return "<section class=\"order-rating order-rating--form\">" +
|
|
6390
|
+
"<h2 class=\"pdp__variants-title\">Rate this order</h2>" +
|
|
6391
|
+
"<p class=\"order-rating__lede\">How did the delivery go? Your feedback helps us improve.</p>" +
|
|
6392
|
+
noticeHtml +
|
|
6393
|
+
"<form class=\"form-stack\" method=\"post\" action=\"/orders/" + esc(String(o.id)) + "/rate\">" +
|
|
6394
|
+
axes +
|
|
6395
|
+
"<label class=\"form-field\"><span>Comment <small>(optional)</small></span>" +
|
|
6396
|
+
"<textarea name=\"comment\" rows=\"3\" maxlength=\"2000\" " +
|
|
6397
|
+
"placeholder=\"Tell us about your delivery experience\"></textarea></label>" +
|
|
6398
|
+
"<div class=\"order-page__actions\">" +
|
|
6399
|
+
"<button type=\"submit\" class=\"btn-primary\">Submit rating</button>" +
|
|
6400
|
+
"</div>" +
|
|
6401
|
+
"</form>" +
|
|
6402
|
+
"</section>";
|
|
6403
|
+
}
|
|
6404
|
+
|
|
6405
|
+
// The submitted rating (the three scores) plus, when present, the
|
|
6406
|
+
// customer's own comment and the operator's public reply. ESCAPE-BY-DEFAULT:
|
|
6407
|
+
// the comment and the reply are spliced from the primitive's pre-escaped
|
|
6408
|
+
// `comment_html` / `response_html` fields (escaped via b.template.escapeHtml
|
|
6409
|
+
// at the primitive's render layer), NEVER the raw `comment` / `response_text`
|
|
6410
|
+
// — so a `<script>`/`onerror` payload a customer typed is inert here. A
|
|
6411
|
+
// flagged comment is suppressed (operators moderate via the admin queue).
|
|
6412
|
+
function _orderRatingDisplay(rating) {
|
|
6413
|
+
var esc = b.template.escapeHtml;
|
|
6414
|
+
var scoreRows = ORDER_RATING_AXES.map(function (axis) {
|
|
6415
|
+
return "<div><dt>" + esc(axis.label) + "</dt>" +
|
|
6416
|
+
"<dd>" + esc(String(rating[axis.field])) + " / 5</dd></div>";
|
|
6417
|
+
}).join("");
|
|
6418
|
+
// comment_html is already escaped by the primitive — splice it, do not
|
|
6419
|
+
// re-escape (double-escaping would render visible entities) and never
|
|
6420
|
+
// reach for rating.comment (the raw, un-escaped string).
|
|
6421
|
+
var commentHtml = (rating.comment_html && !rating.comment_flagged)
|
|
6422
|
+
? "<div class=\"order-rating__comment\"><h3>Your comment</h3>" +
|
|
6423
|
+
"<p>" + rating.comment_html + "</p></div>"
|
|
6424
|
+
: "";
|
|
6425
|
+
// response_html is the operator's public reply, also pre-escaped — splice
|
|
6426
|
+
// it, never rating.response_text.
|
|
6427
|
+
var responseHtml = rating.response_html
|
|
6428
|
+
? "<div class=\"order-rating__response\"><h3>Our reply</h3>" +
|
|
6429
|
+
"<p>" + rating.response_html + "</p></div>"
|
|
6430
|
+
: "";
|
|
6431
|
+
return "<section class=\"order-rating order-rating--done\">" +
|
|
6432
|
+
"<h2 class=\"pdp__variants-title\">Your rating</h2>" +
|
|
6433
|
+
"<dl class=\"order-rating__scores\">" + scoreRows + "</dl>" +
|
|
6434
|
+
commentHtml +
|
|
6435
|
+
responseHtml +
|
|
6436
|
+
"</section>";
|
|
6437
|
+
}
|
|
6438
|
+
|
|
6439
|
+
// Resolve the rating panel for the order page: the submitted rating (read-
|
|
6440
|
+
// only display) when one exists, the submission form when the order is
|
|
6441
|
+
// eligible and unrated, and nothing otherwise (a pending / cancelled /
|
|
6442
|
+
// refunded order, or a renderer reached without the ratings primitive
|
|
6443
|
+
// wired). `opts.rating` is the getRating row (or null); `opts.rating_notice`
|
|
6444
|
+
// is a correction message echoed back onto a rejected submit.
|
|
6445
|
+
function _orderRatingBlock(opts) {
|
|
6446
|
+
var o = opts.order;
|
|
6447
|
+
if (opts.rating) return _orderRatingDisplay(opts.rating);
|
|
6448
|
+
if (opts.rating_eligible && _orderEligibleForRating(o.status)) {
|
|
6449
|
+
return _orderRatingForm(o, opts.rating_notice || null);
|
|
6450
|
+
}
|
|
6451
|
+
return "";
|
|
6452
|
+
}
|
|
6453
|
+
|
|
6454
|
+
// Map a ?rate_err= code (set by a rejected rating POST on its PRG redirect)
|
|
6455
|
+
// to a clean, operator-safe correction message rendered above the rating
|
|
6456
|
+
// form. Defensive request reader — an unknown / absent code yields no
|
|
6457
|
+
// notice, never a raw error or a leak. The duplicate / value / length codes
|
|
6458
|
+
// mirror the primitive's refusal classes without echoing its raw message.
|
|
6459
|
+
function _ratingNoticeFor(code) {
|
|
6460
|
+
if (code === "dupe") return "You've already rated this order.";
|
|
6461
|
+
if (code === "value") return "Please give each rating a score from 1 to 5.";
|
|
6462
|
+
if (code === "comment") return "Your comment couldn't be saved — please shorten it and try again.";
|
|
6463
|
+
if (code === "input") return "Please check your rating and try again.";
|
|
6464
|
+
return null;
|
|
6465
|
+
}
|
|
6466
|
+
|
|
6338
6467
|
function renderOrder(opts) {
|
|
6339
6468
|
if (!opts || !opts.order) throw new TypeError("storefront.renderOrder: opts.order required");
|
|
6340
6469
|
var o = opts.order;
|
|
@@ -6376,6 +6505,10 @@ function renderOrder(opts) {
|
|
|
6376
6505
|
var timelineHtml = _orderTimelineBlock(o.status);
|
|
6377
6506
|
var trackingHtml = _orderTrackingBlock(shipments);
|
|
6378
6507
|
var actionsHtml = _orderActionsBlock(o);
|
|
6508
|
+
// Post-purchase rating panel — container-only (session-gated). The route
|
|
6509
|
+
// passes the resolved getRating row (rating) + the eligibility/notice
|
|
6510
|
+
// flags; the edge render never does, so the panel is empty at the edge.
|
|
6511
|
+
var ratingHtml = _orderRatingBlock(opts);
|
|
6379
6512
|
if (opts.theme) {
|
|
6380
6513
|
return opts.theme.render("order", {
|
|
6381
6514
|
title: "Order " + o.id,
|
|
@@ -6393,6 +6526,7 @@ function renderOrder(opts) {
|
|
|
6393
6526
|
timeline_html: timelineHtml,
|
|
6394
6527
|
tracking_html: trackingHtml,
|
|
6395
6528
|
actions_html: actionsHtml,
|
|
6529
|
+
rating_html: ratingHtml,
|
|
6396
6530
|
can_return: _orderEligibleForReturn(o.status),
|
|
6397
6531
|
can_reorder: _orderEligibleForReorder(o.status),
|
|
6398
6532
|
can_cancel: _orderEligibleForCancel(o.status),
|
|
@@ -6444,6 +6578,11 @@ function renderOrder(opts) {
|
|
|
6444
6578
|
.replace("RAW_ORDER_TRACKING", trackingHtml)
|
|
6445
6579
|
.replace("RAW_ORDER_ACTIONS", actionsHtml)
|
|
6446
6580
|
.replace("RAW_SHIP_TO", _shipToAddressBlock(o.ship_to));
|
|
6581
|
+
// The rating panel carries customer/operator free text (already escaped
|
|
6582
|
+
// into comment_html / response_html, but a `$` in that text would still
|
|
6583
|
+
// trip String.replace's dollar substitution) — splice it via the
|
|
6584
|
+
// replacer-function helper, never a replacement-string .replace.
|
|
6585
|
+
body = _spliceRaw(body, "RAW_ORDER_RATING", ratingHtml);
|
|
6447
6586
|
// Post-purchase cross-sell rail — reuses the catalog grid + product-card
|
|
6448
6587
|
// markup (so it inherits the storefront's card styling), rendered only
|
|
6449
6588
|
// when the picker returned something.
|
|
@@ -10738,12 +10877,35 @@ function mount(router, deps) {
|
|
|
10738
10877
|
}
|
|
10739
10878
|
} catch (_e) { shipments = []; }
|
|
10740
10879
|
}
|
|
10880
|
+
// Post-purchase fulfillment rating. Surfaced only on a signed-in
|
|
10881
|
+
// customer's OWN order (the IDOR gate above already proved
|
|
10882
|
+
// o.customer_id === orderAuth.customer_id when set) — a guest order
|
|
10883
|
+
// carries no owner, so the rating form/display is suppressed there.
|
|
10884
|
+
// Best-effort: an absent ratings primitive (or its table unmigrated)
|
|
10885
|
+
// degrades to "no rating panel" rather than 500-ing the order page.
|
|
10886
|
+
var ratingRow = null;
|
|
10887
|
+
var ratingEligible = false;
|
|
10888
|
+
if (deps.orderRatings && o.customer_id && orderAuth && o.customer_id === orderAuth.customer_id) {
|
|
10889
|
+
// Offer the rating surface only AFTER a successful read: an absent /
|
|
10890
|
+
// unmigrated ratings table degrades to "no rating panel" rather than
|
|
10891
|
+
// rendering a form whose POST would then 500 against the same missing
|
|
10892
|
+
// table. Eligibility also tracks the order-status window so the form
|
|
10893
|
+
// shows only when a submit would actually be accepted (the POST gates
|
|
10894
|
+
// on the same _orderEligibleForRating check).
|
|
10895
|
+
try {
|
|
10896
|
+
ratingRow = await deps.orderRatings.getRating({ order_id: o.id });
|
|
10897
|
+
ratingEligible = _orderEligibleForRating(o.status);
|
|
10898
|
+
} catch (_e) { ratingRow = null; ratingEligible = false; }
|
|
10899
|
+
}
|
|
10741
10900
|
var ordUrl = req.url ? new URL(req.url, "http://localhost") : null;
|
|
10742
10901
|
_send(res, 200, renderOrder({
|
|
10743
10902
|
order: o,
|
|
10744
10903
|
product_lookup: productLookup,
|
|
10745
10904
|
recommendations: recommendations,
|
|
10746
10905
|
shipments: shipments,
|
|
10906
|
+
rating: ratingRow,
|
|
10907
|
+
rating_eligible: ratingEligible,
|
|
10908
|
+
rating_notice: ordUrl ? _ratingNoticeFor(ordUrl.searchParams.get("rate_err")) : null,
|
|
10747
10909
|
reordered: ordUrl ? ordUrl.searchParams.get("reordered") === "1" : false,
|
|
10748
10910
|
cancelled: ordUrl ? ordUrl.searchParams.get("cancelled") === "1" : false,
|
|
10749
10911
|
shop_name: shopName,
|
|
@@ -14661,6 +14823,97 @@ function mount(router, deps) {
|
|
|
14661
14823
|
res.setHeader && res.setHeader("location", "/orders/" + encodeURIComponent(o.id) + "?cancelled=1");
|
|
14662
14824
|
return res.end ? res.end() : res.send("");
|
|
14663
14825
|
});
|
|
14826
|
+
|
|
14827
|
+
// POST /orders/:id/rate — a customer rates one of their OWN orders
|
|
14828
|
+
// (shipping / packaging / recommend, plus an optional comment). The
|
|
14829
|
+
// rating's customer_id is pinned to the SESSION customer — never a
|
|
14830
|
+
// form/query field — and the order_id is verified to belong to that
|
|
14831
|
+
// session before submitRating runs. Two refusals guard against IDOR: an
|
|
14832
|
+
// unauthenticated POST goes to login (never near an order), and a
|
|
14833
|
+
// foreign / guest-owned / unknown / malformed order is a clean 404
|
|
14834
|
+
// (never rated, never leaked). Bad input (rating out of [1,5], over-
|
|
14835
|
+
// length comment) and a duplicate submission map to a clean correction
|
|
14836
|
+
// redirect, never a 500 or a raw-error leak. Mounts only when the
|
|
14837
|
+
// ratings primitive is wired.
|
|
14838
|
+
if (deps.orderRatings) {
|
|
14839
|
+
router.post("/orders/:order_id/rate", async function (req, res) {
|
|
14840
|
+
var orderId = req.params && req.params.order_id;
|
|
14841
|
+
// Rating is a logged-in-customer action — resolve the session first
|
|
14842
|
+
// so an unauthenticated POST goes to login, never near the order.
|
|
14843
|
+
var rateAuth = _currentCustomerEnv(req);
|
|
14844
|
+
if (!rateAuth) {
|
|
14845
|
+
res.status(303); res.setHeader && res.setHeader("location", "/account/login");
|
|
14846
|
+
return res.end ? res.end() : res.send("");
|
|
14847
|
+
}
|
|
14848
|
+
var o;
|
|
14849
|
+
try { o = orderId ? await deps.order.get(orderId) : null; }
|
|
14850
|
+
catch (e) { if (e instanceof TypeError) { o = null; } else throw e; }
|
|
14851
|
+
// Ownership gate against IDOR: a missing order, a malformed id, an
|
|
14852
|
+
// order owned by another customer, OR a guest order with no owner
|
|
14853
|
+
// all 404 — the rating never touches an order the caller doesn't own.
|
|
14854
|
+
if (!o || !o.customer_id || o.customer_id !== rateAuth.customer_id) {
|
|
14855
|
+
return _send(res, 404, renderNotFound({ shop_name: shopName, theme: theme }));
|
|
14856
|
+
}
|
|
14857
|
+
// Eligibility gate mirrors the rating window (paid → delivered). A
|
|
14858
|
+
// pending / cancelled / refunded order bounces back unchanged.
|
|
14859
|
+
if (!_orderEligibleForRating(o.status)) {
|
|
14860
|
+
res.status(303);
|
|
14861
|
+
res.setHeader && res.setHeader("location", "/orders/" + encodeURIComponent(o.id));
|
|
14862
|
+
return res.end ? res.end() : res.send("");
|
|
14863
|
+
}
|
|
14864
|
+
var body = req.body || {};
|
|
14865
|
+
// Defensive request-shape readers: the rating axes arrive as form
|
|
14866
|
+
// strings — coerce to integers so the primitive's strict [1,5]
|
|
14867
|
+
// integer validator sees a number (a blank / garbage field becomes
|
|
14868
|
+
// NaN, which the primitive refuses as a clean TypeError → "value").
|
|
14869
|
+
function _scoreField(raw) {
|
|
14870
|
+
// Whole-string integer only. parseInt would silently accept a
|
|
14871
|
+
// crafted "5abc" (→5) or "1.9" (→1) and persist a value the
|
|
14872
|
+
// shopper never chose; require the entire field to be digits so
|
|
14873
|
+
// anything else becomes NaN, which the primitive's strict [1,5]
|
|
14874
|
+
// validator refuses as a clean "value" correction.
|
|
14875
|
+
var s = String(raw == null ? "" : raw).trim();
|
|
14876
|
+
if (!/^-?\d+$/.test(s)) return NaN;
|
|
14877
|
+
var n = Number(s);
|
|
14878
|
+
return Number.isInteger(n) ? n : NaN;
|
|
14879
|
+
}
|
|
14880
|
+
function _rateRedirect(code) {
|
|
14881
|
+
res.status(303);
|
|
14882
|
+
res.setHeader && res.setHeader("location",
|
|
14883
|
+
"/orders/" + encodeURIComponent(o.id) + (code ? "?rate_err=" + code : ""));
|
|
14884
|
+
return res.end ? res.end() : res.send("");
|
|
14885
|
+
}
|
|
14886
|
+
var comment = typeof body.comment === "string" && body.comment.length ? body.comment : undefined;
|
|
14887
|
+
try {
|
|
14888
|
+
await deps.orderRatings.submitRating({
|
|
14889
|
+
order_id: o.id,
|
|
14890
|
+
customer_id: rateAuth.customer_id, // session-pinned, never from the form
|
|
14891
|
+
shipping_rating: _scoreField(body.shipping_rating),
|
|
14892
|
+
packaging_rating: _scoreField(body.packaging_rating),
|
|
14893
|
+
recommend_rating: _scoreField(body.recommend_rating),
|
|
14894
|
+
comment: comment,
|
|
14895
|
+
});
|
|
14896
|
+
} catch (e) {
|
|
14897
|
+
// Coded errors first (a plain Error with .code), then the
|
|
14898
|
+
// primitive's TypeErrors — so a duplicate surfaces its own notice
|
|
14899
|
+
// rather than the generic bad-input one. Any of these is a clean
|
|
14900
|
+
// correction redirect, never a 500.
|
|
14901
|
+
if (e && e.code === "ORDER_RATING_ALREADY_EXISTS") return _rateRedirect("dupe");
|
|
14902
|
+
if (e instanceof TypeError) {
|
|
14903
|
+
// The comment validator's message names "comment"; everything
|
|
14904
|
+
// else is a rating-value problem. Either way, no raw leak.
|
|
14905
|
+
var isComment = /comment/.test(String(e.message || ""));
|
|
14906
|
+
return _rateRedirect(isComment ? "comment" : "value");
|
|
14907
|
+
}
|
|
14908
|
+
throw e;
|
|
14909
|
+
}
|
|
14910
|
+
// PRG back to the order page — the submitted rating now renders in
|
|
14911
|
+
// place of the form (a refresh doesn't re-submit).
|
|
14912
|
+
res.status(303);
|
|
14913
|
+
res.setHeader && res.setHeader("location", "/orders/" + encodeURIComponent(o.id));
|
|
14914
|
+
return res.end ? res.end() : res.send("");
|
|
14915
|
+
});
|
|
14916
|
+
}
|
|
14664
14917
|
}
|
|
14665
14918
|
|
|
14666
14919
|
// POST /cart/lines — add a line. Reads variant_id + qty from the
|
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"_about": "blamejs.shop vendors a single framework — blamejs — which itself bundles every server-side crypto/identity dependency. The transitive packages blamejs ships are surfaced in its own MANIFEST.json at lib/vendor/blamejs/lib/vendor/MANIFEST.json — Trivy / Grype rely on that nested data for CVE attribution.",
|
|
4
4
|
"packages": {
|
|
5
5
|
"blamejs": {
|
|
6
|
-
"version": "0.14.
|
|
7
|
-
"tag": "v0.14.
|
|
6
|
+
"version": "0.14.16",
|
|
7
|
+
"tag": "v0.14.16",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.14.x
|
|
10
10
|
|
|
11
|
+
- v0.14.16 (2026-05-31) — **Connection entry-point ports are validated at config time.** Six connection entry points previously read opts.port with a bare `|| <default>` fallback, silently coercing a string, negative, NaN, or out-of-range port instead of catching the operator's typo. A new b.validateOpts.optionalPort enforces the RFC 6335 §6 wire-valid range and is wired into b.mail.smtpTransport, b.ntpCheck.querySingle, b.networkDns.useDnsOverTls, b.networkNts (KE handshake / query / facade), b.redisClient.create, and createApp().listen — each now throws at construction with a clear message naming the bad value. The app.listen / createApp bind site opts into allowZero so port 0 (the legitimate ephemeral-bind sentinel) still works; the five outbound-connect sites require [1,65535]. **Added:** *`b.validateOpts.optionalPort`* — A config-time port validator: `optionalPort(value, label, errorClass, code, opts?)` returns an omitted (`undefined` / `null`) port unchanged, and otherwise requires an integer in the RFC 6335 §6 wire-valid range [1,65535] — rejecting a string, negative, NaN, Infinity, fractional, or out-of-range value. Pass `{ allowZero: true }` for a listen-bind site where port 0 is the OS ephemeral-bind sentinel. The thrown message reports the offending shape (so `Infinity` / `"443"` stay visible), and routes a caller-supplied typed framework error (or a plain Error when none is given), matching the existing `optionalPositiveFinite` family. **Changed:** *Connection entry points reject a malformed port at construction* — `b.mail.smtpTransport`, `b.ntpCheck.querySingle`, `b.networkDns.useDnsOverTls`, `b.networkNts.performKeHandshake` / `query` / `querySingle`, `b.redisClient.create`, and `createApp().listen` (plus the `createApp` constructor's default port) now validate `opts.port` and throw synchronously on a non-integer / out-of-range value rather than coercing it through `||` to a default. This is a behavior change for a caller that was passing a non-canonical port (e.g. the string `"587"` or a NaN) and relying on the silent fallback — pass an integer in [1,65535] instead (or `0` for an ephemeral `createApp().listen` bind). `b.ntpCheck` gains a typed `NtpCheckError` for this (it had no error class before). **Detectors:** *Connection entry points must compose the port validator* — A new check flags a lib connection entry point that reads `opts.port` / `opts.kePort` / `opts.ntpPort` with a `|| <default>` fallback without composing `b.validateOpts.optionalPort` (or the equivalent `numericBounds.isPositiveFiniteInt` + 65535 cap), so an unvalidated port read can't slip back in. **Migration:** *Pass an integer port to connection primitives* — If you were passing a non-integer or out-of-range `opts.port` to a mail / NTP / NTS / DNS-over-TLS / Redis transport or to `createApp().listen` and relying on the silent `|| default` fallback, that now throws at construction. Pass an integer in [1,65535]; for an ephemeral `createApp().listen` bind, pass `0` (still accepted).
|
|
12
|
+
|
|
11
13
|
- v0.14.14 (2026-05-31) — **Recognized consent purposes with lawful-basis gating, and a new b.privacy namespace for annual EdTech vendor-review attestations.** Closes the student-data gap where an educational-only consent purpose and an annual third-party vendor-review report were described but never implemented. b.consent gains a recognized-purpose vocabulary: a purpose value matching a recognized key carries lawful-basis constraints that grant() enforces, and the named educational-only purpose (FERPA's school-official exception and California's SOPIPA) refuses a legitimate_interests lawful basis. The new b.privacy namespace ships vendorReview(), a builder for the dated, clause-by-clause annual EdTech third-party / processor review FERPA and SOPIPA expect a school or district to keep — it computes whether every required clause (no targeted advertising, no commercial profiling, no sale of student data, deletion on request, school-official designation, and so on) is attested, names the gaps, and stamps a 365-day re-review clock. Free-form consent purposes keep working unchanged, so the vocabulary is opt-in and additive. **Added:** *`b.consent` recognized-purpose vocabulary + lawful-basis gating* — `b.consent.recognizedPurpose(name)` looks up a recognized purpose and `b.consent.listPurposes()` enumerates them. When a `grant({ purpose })` value matches a recognized key, `grant()` enforces that purpose's lawful-basis constraints; the `educational-only` purpose forbids a `legitimate_interests` basis (FERPA 34 CFR 99.31(a)(1) school-official exception; California SOPIPA Cal. B&P 22584; FTC school-authorized COPPA consent 16 CFR 312.5(c)(10)) and marks the data commercial-use-prohibited. The commercial-use prohibition is an operator trust-boundary obligation — `isGranted()` does not re-derive it. Any purpose value NOT in the vocabulary stays free-form and unconstrained, so existing callers are unaffected; the hash-chain column set is unchanged, so `b.consent.verify()` over existing rows is unaffected. · *`b.privacy.vendorReview` — annual EdTech vendor-review attestation* — A new `b.privacy` namespace whose `vendorReview(opts)` builds the dated third-party / processor review a FERPA school-official arrangement and California SOPIPA expect for every vendor that touches student data. The operator supplies a boolean attestation per clause (educational-purpose-only, no-targeted-advertising, no-commercial-profiling, no-sale-of-student-data, security-safeguards, deletion-on-request, sub-processor-currency, breach-notification, school-official-designation, directory-information-handling); `vendorReview` validates the shape, computes whether every required clause is attested (`attested`) and which are not (`gaps`), and stamps `reviewedAt` plus a 365-day `nextReviewDueAt` re-review clock. `b.privacy.listVendorReviewClauses()` returns the clause set with citations. Operator-feeds-metadata: the frozen report is not framework-persisted — compose it into your retention / audit / export sink. A best-effort `privacy.vendor_review.recorded` audit event fires when an audit sink is wired. **Detectors:** *A gated consent purpose must go through `b.consent`* — A new check flags any lib code that mints a consent row with a hardcoded `educational-only` purpose literal without composing the recognized-purpose vocabulary — which would record the value while never enforcing its FERPA / SOPIPA lawful-basis constraint.
|
|
12
14
|
|
|
13
15
|
- v0.14.13 (2026-05-31) — **Close advertised-but-missing surface: SRS1 chained forwarding, DCQL array-wildcard claim paths, and in-memory safe-archive extraction.** Three primitives advertised a capability in their documentation or card but refused or omitted it at runtime; this release implements each. b.mail.srs gains srs1Rewrite for the SRS1 double-forward (and multi-hop) case — previously the @intro described SRS1 and create() threw, pointing at a function that was never exported. b.safeArchive gains extractToMemory, the in-memory counterpart to extract for read-only / serverless filesystems — previously the card advertised in-memory extraction but the orchestrator required a destination directory. b.auth.oid4vp.matchDcql now honours a null claims-path segment as the array wildcard the OpenID4VP DCQL spec defines, rather than refusing it as unsupported while the card advertised DCQL. A stale version-pinned wording in a safe-archive error message is corrected. Every change is additive or message-only — no existing caller changes behaviour. **Added:** *`b.mail.srs` SRS1 chained forwarding — `srs1Rewrite`* — `b.mail.srs.create(...)` now returns `srs1Rewrite` alongside `rewrite` / `reverse`. `srs1Rewrite(srsAddress)` chains an already-SRS0 (or SRS1) envelope-from for a further forwarding hop: it keeps the original SRS0 body verbatim, prepends the SRS0 originator's domain, and binds the pair with this forwarder's own HMAC-SHA-256 tag — no new timestamp, no repeated original local-part — emitting `SRS1=tag=originator==<SRS0-body>@thisForwarder`. `reverse()` now detects an SRS1 address, verifies this hop's tag and forwarder-domain binding, and unwraps exactly one hop back to the originator's SRS0 so a multi-hop bounce routes straight to the forwarder that can recover the original sender. Typed failure modes: `srs/not-srs0` (input not SRS-encoded), `srs/malformed` (missing the `==` separator), `srs/bad-tag` (tampered), `srs/too-long` (chain exceeds the RFC 5321 256-octet path limit). Implements the Sender Rewriting Scheme SRS1 wire format; the second-hop SPF rationale is RFC 7208 §2.4. · *`b.safeArchive.extractToMemory` — in-memory safe extraction* — An async generator counterpart to `b.safeArchive.extract` for read-only / serverless filesystems: it resolves the source, sniffs the format, auto-unwraps recipient (`BAWRP`) / passphrase (`BAWPP`) envelopes, and dispatches to the zip / tar / tar.gz reader's in-memory `extractEntries()`, yielding `{ name, bytes, size }` per regular-file entry without ever writing to disk. It takes no `destination`. Every defense the disk path runs applies unchanged: the zip-bomb caps (entry-count / per-entry / total / expansion-ratio), the `b.guardArchive` metadata cascade (Zip-Slip / path-traversal / symlink-escape / encrypted-entry refusal, CVE-2025-3445 class), and the entry-type policy. The disk-only realpath-agreement check (CVE-2025-4517 PATH_MAX TOCTOU defense) is intentionally absent — there is no extraction root — so the archive-level name refusals carry containment. Trusted-stream sources are refused upfront (the adversarial-safe central-directory walk needs random access). gzip magic per RFC 1952 §2.3.1. **Fixed:** *OID4VP DCQL `null` claim-path segment now resolves the array wildcard* — `b.auth.oid4vp.matchDcql` previously threw `auth-oid4vp/null-path-segment-not-supported` for a `null` claims-path segment while the namespace card advertised DCQL — under-disclosing a legitimate presentation (CWE-863). Per OpenID4VP 1.0 §7.1.1 a `null` segment selects all elements of the array at that depth; the matcher now recurses over array elements with existence semantics (with DCQL value-matching applied to any selected leaf), composed to arbitrary depth. A `null` segment on a non-array node — like an integer index into a non-array, or a string key into an array — is a clean non-match, not a thrown error, because the matcher walks holder credential data rather than operator config. String and integer claim paths are byte-identical to before; only queries that previously threw now succeed or fail cleanly. · *safe-archive trusted-stream refusal message no longer cites a stale version* — The thrown `safe-archive/trusted-stream-unsupported` message and its comment claimed trusted-stream extraction was "deferred to v0.12.8 / when the v0.12.8 sequential extract path lands." That path shipped long ago — `b.archive.read.zip.fromTrustedStream` and the tar sequential mode exist — so the message now points at them as present capabilities and drops the version-pinned wording. The error code is unchanged. **Detectors:** *A primitive may not advertise a capability and then throw an unimplemented stub* — A new check flags a bare `not yet supported` / `operator demand TBD` / `not supported in v1` refusal in a lib throw string (comments excluded). A defer is only complete with a written re-open condition; the SRS1 and DCQL stubs that this release implements both carried this bare-defer shape, and the detector keeps it from re-entering. · *DCQL `null` path segments must recurse, never refuse* — A new check flags the `null path segment not supported` refusal shape in `lib/auth/oid4vp.js`, so the spec-mandated array wildcard cannot be re-stubbed. · *`extractToMemory` must stay disk-free* — A new check flags any `writeFileSync` / `renameSync` / `mkdirSync` / `createWriteStream` inside the `extractToMemory` generator body, so the read-only / serverless contract cannot regress into a disk write.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"frameworkVersion": "0.14.
|
|
4
|
-
"createdAt": "2026-
|
|
3
|
+
"frameworkVersion": "0.14.16",
|
|
4
|
+
"createdAt": "2026-06-01T01:11:20.010Z",
|
|
5
5
|
"exports": {
|
|
6
6
|
"a2a": {
|
|
7
7
|
"type": "object",
|
|
@@ -43286,6 +43286,10 @@
|
|
|
43286
43286
|
"type": "primitive",
|
|
43287
43287
|
"valueType": "number"
|
|
43288
43288
|
},
|
|
43289
|
+
"NtpCheckError": {
|
|
43290
|
+
"type": "function",
|
|
43291
|
+
"arity": 4
|
|
43292
|
+
},
|
|
43289
43293
|
"bootCheck": {
|
|
43290
43294
|
"type": "function",
|
|
43291
43295
|
"arity": 1
|
|
@@ -93,6 +93,7 @@ var nodeFs = require("node:fs");
|
|
|
93
93
|
var nodePath = require("node:path");
|
|
94
94
|
var appShutdown = require("./app-shutdown");
|
|
95
95
|
var audit = require("./audit");
|
|
96
|
+
var validateOpts = require("./validate-opts");
|
|
96
97
|
var C = require("./constants");
|
|
97
98
|
var cluster = require("./cluster");
|
|
98
99
|
var db = require("./db");
|
|
@@ -139,6 +140,9 @@ async function createApp(opts) {
|
|
|
139
140
|
if (!opts.dataDir || typeof opts.dataDir !== "string") {
|
|
140
141
|
throw new Error("createApp: opts.dataDir is required");
|
|
141
142
|
}
|
|
143
|
+
// Constructor-time default port (used by listen() when listenOpts.port is
|
|
144
|
+
// omitted); allowZero for the ephemeral-bind sentinel.
|
|
145
|
+
validateOpts.optionalPort(opts.port, "createApp: opts.port", undefined, undefined, { allowZero: true });
|
|
142
146
|
var dataDir = nodePath.resolve(opts.dataDir);
|
|
143
147
|
if (!nodeFs.existsSync(dataDir)) {
|
|
144
148
|
nodeFs.mkdirSync(dataDir, { recursive: true });
|
|
@@ -279,6 +283,10 @@ async function createApp(opts) {
|
|
|
279
283
|
|
|
280
284
|
function listen(listenOpts) {
|
|
281
285
|
listenOpts = listenOpts || {};
|
|
286
|
+
// Port 0 is the legitimate ephemeral-bind sentinel for a listen socket
|
|
287
|
+
// (RFC 6335 §6 / POSIX bind), so allowZero — but a non-integer / NaN /
|
|
288
|
+
// out-of-range port is an operator typo that must fail at boot.
|
|
289
|
+
validateOpts.optionalPort(listenOpts.port, "createApp.listen: listenOpts.port", undefined, undefined, { allowZero: true });
|
|
282
290
|
var port = (listenOpts.port !== undefined) ? listenOpts.port
|
|
283
291
|
: (opts.port !== undefined) ? opts.port
|
|
284
292
|
: 0;
|
|
@@ -767,6 +767,7 @@ function smtpTransport(opts) {
|
|
|
767
767
|
"dkimSigner must be an object with a .sign(rfc822) method " +
|
|
768
768
|
"(see b.mail.dkim.create)", true);
|
|
769
769
|
}
|
|
770
|
+
validateOpts.optionalPort(opts.port, "smtp transport: opts.port", MailError, "mail/smtp-misconfigured");
|
|
770
771
|
var port = opts.port || 587;
|
|
771
772
|
var useImplicitTLS = port === 465 || opts.implicitTls === true;
|
|
772
773
|
var rejectUnauthorized = opts.rejectUnauthorized !== false;
|
|
@@ -253,6 +253,7 @@ function useDnsOverTls(opts) {
|
|
|
253
253
|
opts = opts || {};
|
|
254
254
|
validateOpts(opts, ["host", "port", "servername", "ca"], "dns.useDnsOverTls");
|
|
255
255
|
validateOpts.requireNonEmptyString(opts.host, "dns.useDnsOverTls: host", DnsError, "dns/bad-dot-host");
|
|
256
|
+
validateOpts.optionalPort(opts.port, "dns.useDnsOverTls: opts.port", DnsError, "dns/bad-dot-port");
|
|
256
257
|
if (opts.ca !== undefined && opts.ca !== null &&
|
|
257
258
|
!Buffer.isBuffer(opts.ca) && typeof opts.ca !== "string" && !Array.isArray(opts.ca)) {
|
|
258
259
|
throw new DnsError("dns/bad-dot-ca",
|
|
@@ -241,6 +241,7 @@ function performKeHandshake(opts) {
|
|
|
241
241
|
opts = opts || {};
|
|
242
242
|
validateOpts(opts, ["host", "port", "servername", "aead", "ca", "timeoutMs"], "nts.performKeHandshake");
|
|
243
243
|
validateOpts.requireNonEmptyString(opts.host, "nts.performKeHandshake: host", NtsError, "nts/bad-host");
|
|
244
|
+
validateOpts.optionalPort(opts.port, "nts.performKeHandshake: opts.port", NtsError, "nts/bad-ke-port");
|
|
244
245
|
var timeoutMs = opts.timeoutMs || C.TIME.seconds(10);
|
|
245
246
|
return new Promise(function (resolve, reject) {
|
|
246
247
|
var settled = false;
|
|
@@ -408,6 +409,7 @@ function _walkExtensions(msg, startOff) {
|
|
|
408
409
|
function querySingle(opts) {
|
|
409
410
|
opts = opts || {};
|
|
410
411
|
validateOpts(opts, ["host", "port", "aeadId", "c2sKey", "s2cKey", "cookies", "timeoutMs"], "nts.querySingle");
|
|
412
|
+
validateOpts.optionalPort(opts.port, "nts.querySingle: opts.port", NtsError, "nts/bad-ntp-port");
|
|
411
413
|
if (!Buffer.isBuffer(opts.c2sKey) || opts.c2sKey.length === 0) {
|
|
412
414
|
throw new NtsError("nts/no-c2s-key", "nts.querySingle: c2sKey required (Buffer)");
|
|
413
415
|
}
|
|
@@ -542,6 +544,8 @@ function querySingle(opts) {
|
|
|
542
544
|
async function query(opts) {
|
|
543
545
|
opts = opts || {};
|
|
544
546
|
validateOpts(opts, ["host", "kePort", "ntpPort", "aead", "ca", "timeoutMs", "servername"], "nts.query");
|
|
547
|
+
validateOpts.optionalPort(opts.kePort, "nts.query: opts.kePort", NtsError, "nts/bad-ke-port");
|
|
548
|
+
validateOpts.optionalPort(opts.ntpPort, "nts.query: opts.ntpPort", NtsError, "nts/bad-ntp-port");
|
|
545
549
|
var ke = await performKeHandshake({
|
|
546
550
|
host: opts.host,
|
|
547
551
|
port: opts.kePort,
|
|
@@ -48,10 +48,16 @@ var dgram = require("node:dgram");
|
|
|
48
48
|
var C = require("./constants");
|
|
49
49
|
var lazyRequire = require("./lazy-require");
|
|
50
50
|
var safeAsync = require("./safe-async");
|
|
51
|
+
var validateOpts = require("./validate-opts");
|
|
52
|
+
var { defineClass } = require("./framework-error");
|
|
51
53
|
|
|
52
54
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
53
55
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
54
56
|
|
|
57
|
+
// Config-time misuse (a bad opts.port) throws a typed, permanent error so an
|
|
58
|
+
// operator catches the typo at boot rather than as a Promise rejection.
|
|
59
|
+
var NtpCheckError = defineClass("NtpCheckError", { alwaysPermanent: true });
|
|
60
|
+
|
|
55
61
|
// NTP epoch: 1900-01-01. Unix epoch: 1970-01-01. Offset: 70 years incl. 17
|
|
56
62
|
// leap days = 2,208,988,800 seconds.
|
|
57
63
|
var NTP_TO_UNIX_OFFSET_SECONDS = 2208988800;
|
|
@@ -166,6 +172,7 @@ function _resetThresholdsForTest() {
|
|
|
166
172
|
*/
|
|
167
173
|
function querySingle(server, opts) {
|
|
168
174
|
opts = opts || {};
|
|
175
|
+
validateOpts.optionalPort(opts.port, "ntpCheck.querySingle: opts.port", NtpCheckError, "ntp/bad-port");
|
|
169
176
|
var port = opts.port || DEFAULT_PORT;
|
|
170
177
|
var timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
171
178
|
|
|
@@ -445,6 +452,7 @@ function monitor(opts) {
|
|
|
445
452
|
|
|
446
453
|
module.exports = {
|
|
447
454
|
querySingle: querySingle,
|
|
455
|
+
NtpCheckError: NtpCheckError,
|
|
448
456
|
checkDrift: checkDrift,
|
|
449
457
|
bootCheck: bootCheck,
|
|
450
458
|
monitor: monitor,
|
|
@@ -155,9 +155,17 @@ function _frameToValue(frame) {
|
|
|
155
155
|
function create(opts) {
|
|
156
156
|
opts = opts || {};
|
|
157
157
|
validateOpts.requireNonEmptyString(opts.url, "redis.create: opts.url", RedisError, "BAD_OPTS");
|
|
158
|
+
// Validate an operator-supplied opts.port up front for a clear typo
|
|
159
|
+
// message (e.g. the string "6379" or a negative value).
|
|
160
|
+
validateOpts.optionalPort(opts.port, "redis.create: opts.port", RedisError, "BAD_OPTS");
|
|
158
161
|
var parsed = _parseRedisUrl(opts.url);
|
|
159
162
|
var host = opts.host || parsed.host;
|
|
160
163
|
var port = opts.port || parsed.port;
|
|
164
|
+
// Re-validate the RESOLVED port. A url-supplied port (redis://h:0,
|
|
165
|
+
// redis://h:99999) is not range-checked by _parseRedisUrl, so without
|
|
166
|
+
// this an outbound connect could inherit a zero / out-of-range port that
|
|
167
|
+
// the opts.port guard above never sees.
|
|
168
|
+
validateOpts.optionalPort(port, "redis.create: resolved port (opts.port or url)", RedisError, "BAD_OPTS");
|
|
161
169
|
var useTls = opts.tls !== undefined ? !!opts.tls : parsed.tls;
|
|
162
170
|
var password = opts.password !== undefined ? opts.password : parsed.password;
|
|
163
171
|
var username = opts.username !== undefined ? opts.username : parsed.username;
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
* a typed error wrap the call.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
+
var numericBounds = require("./numeric-bounds");
|
|
31
|
+
|
|
30
32
|
function _format(primitive, unknownKey, allowedKeys) {
|
|
31
33
|
return primitive + ": unknown option '" + unknownKey + "'. " +
|
|
32
34
|
"Allowed keys: " + allowedKeys.slice().sort().join(", ") + ".";
|
|
@@ -150,6 +152,26 @@ function optionalFunction(value, label, errorClass, code) {
|
|
|
150
152
|
return value;
|
|
151
153
|
}
|
|
152
154
|
|
|
155
|
+
// optionalPort — a TCP/UDP port number must be an integer in the wire-valid
|
|
156
|
+
// range (RFC 6335 §6). Outbound-connect sites require [1,65535]; pass
|
|
157
|
+
// { allowZero: true } for a listen-bind site where port 0 is the legitimate
|
|
158
|
+
// ephemeral-bind sentinel the OS replaces with a kernel-assigned port. Uses
|
|
159
|
+
// numericBounds.shape() in the message so Infinity / NaN / "443" stay visible.
|
|
160
|
+
function optionalPort(value, label, errorClass, code, opts) {
|
|
161
|
+
if (value === undefined || value === null) return value;
|
|
162
|
+
opts = opts || {};
|
|
163
|
+
var ok = opts.allowZero
|
|
164
|
+
? (numericBounds.isNonNegativeFiniteInt(value) && value <= 65535)
|
|
165
|
+
: (numericBounds.isPositiveFiniteInt(value) && value <= 65535);
|
|
166
|
+
if (!ok) {
|
|
167
|
+
_throw(errorClass, code, (label || "opt") + " must be " +
|
|
168
|
+
(opts.allowZero ? "0 (ephemeral) or " : "") +
|
|
169
|
+
"an integer in [" + (opts.allowZero ? 0 : 1) + ",65535], got " + numericBounds.shape(value),
|
|
170
|
+
"validate-opts/bad-port");
|
|
171
|
+
}
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
|
|
153
175
|
// applyDefaults — resolve every key in DEFAULTS against opts. For each
|
|
154
176
|
// key, the operator's value (if not undefined) wins; otherwise the
|
|
155
177
|
// default is used. Returns a new plain object — NOT a frozen one, so
|
|
@@ -391,6 +413,7 @@ module.exports.optionalBoolean = optionalBoolean;
|
|
|
391
413
|
module.exports.optionalPositiveInt = optionalPositiveInt;
|
|
392
414
|
module.exports.optionalFiniteNonNegative = optionalFiniteNonNegative;
|
|
393
415
|
module.exports.optionalPositiveFinite = optionalPositiveFinite;
|
|
416
|
+
module.exports.optionalPort = optionalPort;
|
|
394
417
|
module.exports.optionalFunction = optionalFunction;
|
|
395
418
|
module.exports.optionalNonEmptyString = optionalNonEmptyString;
|
|
396
419
|
module.exports.optionalNonEmptyStringArray = optionalNonEmptyStringArray;
|