@blamejs/blamejs-shop 0.3.35 → 0.3.36
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 +2 -0
- package/lib/admin.js +375 -1
- package/lib/asset-manifest.json +1 -1
- package/lib/storefront.js +450 -0
- package/lib/vendor/MANIFEST.json +3 -3
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +14 -2
- package/lib/vendor/blamejs/lib/crypto-field.js +69 -0
- package/lib/vendor/blamejs/lib/mail-store-fts.js +40 -18
- package/lib/vendor/blamejs/lib/mail-store.js +188 -2
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.10.json +54 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +17 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +44 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-store-fts.test.js +208 -0
- package/package.json +1 -1
package/lib/storefront.js
CHANGED
|
@@ -3462,6 +3462,206 @@ function renderReturns(opts) {
|
|
|
3462
3462
|
});
|
|
3463
3463
|
}
|
|
3464
3464
|
|
|
3465
|
+
// Customer-facing reasons for an exchange. The values mirror the
|
|
3466
|
+
// order-exchanges module's REASONS allow-list; the backend validates the
|
|
3467
|
+
// submitted value against its own list, so a forged value is refused
|
|
3468
|
+
// there. (No-longer-needed isn't an exchange reason — that's a return.)
|
|
3469
|
+
var EXCHANGE_REASONS = [
|
|
3470
|
+
["defective", "Defective / doesn't work"],
|
|
3471
|
+
["wrong-item", "Wrong item received"],
|
|
3472
|
+
["wrong-size", "Wrong size"],
|
|
3473
|
+
["wrong-colour", "Wrong colour"],
|
|
3474
|
+
["damaged-in-transit", "Damaged in transit"],
|
|
3475
|
+
["not-as-described", "Not as described"],
|
|
3476
|
+
["other", "Other"],
|
|
3477
|
+
];
|
|
3478
|
+
|
|
3479
|
+
// Customer-facing phrasing for an exchange's FSM status. The module's
|
|
3480
|
+
// enum (pending / approved / shipped / delivered / received / closed /
|
|
3481
|
+
// rejected) is operator vocabulary; these read for the shopper.
|
|
3482
|
+
var EXCHANGE_STATUS_LABELS = {
|
|
3483
|
+
pending: "Requested",
|
|
3484
|
+
approved: "Approved",
|
|
3485
|
+
shipped: "Replacement shipped",
|
|
3486
|
+
delivered: "Replacement delivered",
|
|
3487
|
+
received: "Return received",
|
|
3488
|
+
closed: "Completed",
|
|
3489
|
+
rejected: "Declined",
|
|
3490
|
+
};
|
|
3491
|
+
|
|
3492
|
+
function _exchangeStatusBadge(status) {
|
|
3493
|
+
var esc = b.template.escapeHtml;
|
|
3494
|
+
var label = EXCHANGE_STATUS_LABELS[status] || status;
|
|
3495
|
+
return "<span class=\"return-status return-status--" + esc(String(status)) + "\">" +
|
|
3496
|
+
esc(String(label)) + "</span>";
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
// Customer-facing exchange-request form for one owned order. `opts.lines`
|
|
3500
|
+
// is the order's order_lines, each decorated with `replacement_options`
|
|
3501
|
+
// (the sibling variants of that line's product the shopper can swap to).
|
|
3502
|
+
// `opts.notice` is an optional error bounced back from a failed POST.
|
|
3503
|
+
// Reuses the returns-form layout classes — no new CSS.
|
|
3504
|
+
function renderExchangeForm(opts) {
|
|
3505
|
+
var esc = b.template.escapeHtml;
|
|
3506
|
+
var order = opts.order;
|
|
3507
|
+
var lines = opts.lines || [];
|
|
3508
|
+
var lineRows = "";
|
|
3509
|
+
for (var i = 0; i < lines.length; i += 1) {
|
|
3510
|
+
var l = lines[i];
|
|
3511
|
+
var maxQty = Number(l.qty) || 1;
|
|
3512
|
+
var replOpts = (l.replacement_options || []).map(function (o) {
|
|
3513
|
+
return "<option value=\"" + esc(String(o.id)) + "\">" + esc(String(o.label)) + "</option>";
|
|
3514
|
+
}).join("");
|
|
3515
|
+
// A line with no resolvable sibling variants can't be exchanged for a
|
|
3516
|
+
// different option — offer a same-SKU swap (replace a defective unit)
|
|
3517
|
+
// so the line is never silently undisplayable.
|
|
3518
|
+
var replaceField = replOpts
|
|
3519
|
+
? "<label class=\"return-line__qty\">Swap for " +
|
|
3520
|
+
"<select name=\"replacement_" + esc(l.id) + "\">" + replOpts + "</select>" +
|
|
3521
|
+
"</label>"
|
|
3522
|
+
: "<p class=\"return-line__of\">Same item, replacement unit.</p>";
|
|
3523
|
+
lineRows +=
|
|
3524
|
+
"<li class=\"return-line\">" +
|
|
3525
|
+
"<label class=\"return-line__pick\">" +
|
|
3526
|
+
"<input type=\"radio\" name=\"line_id\" value=\"" + esc(l.id) + "\"" + (i === 0 ? " checked" : "") + ">" +
|
|
3527
|
+
"<span class=\"return-line__sku\"><code>" + esc(l.sku) + "</code></span>" +
|
|
3528
|
+
"</label>" +
|
|
3529
|
+
"<label class=\"return-line__qty\">Qty to exchange " +
|
|
3530
|
+
"<input type=\"number\" name=\"qty_" + esc(l.id) + "\" value=\"1\" min=\"1\" max=\"" + maxQty + "\">" +
|
|
3531
|
+
" <span class=\"return-line__of\">of " + maxQty + "</span>" +
|
|
3532
|
+
"</label>" +
|
|
3533
|
+
replaceField +
|
|
3534
|
+
"</li>";
|
|
3535
|
+
}
|
|
3536
|
+
var reasonOpts = EXCHANGE_REASONS.map(function (r) {
|
|
3537
|
+
return "<option value=\"" + esc(r[0]) + "\">" + esc(r[1]) + "</option>";
|
|
3538
|
+
}).join("");
|
|
3539
|
+
var notice = opts.notice
|
|
3540
|
+
? "<p class=\"form-notice form-notice--error\" role=\"alert\">" + esc(String(opts.notice)) + "</p>"
|
|
3541
|
+
: "";
|
|
3542
|
+
var body =
|
|
3543
|
+
"<section class=\"return-form-page\">" +
|
|
3544
|
+
"<nav class=\"breadcrumb\" aria-label=\"Breadcrumb\"><ol>" +
|
|
3545
|
+
"<li><a href=\"/account\">Account</a></li>" +
|
|
3546
|
+
"<li><a href=\"/account/exchanges\">Exchanges</a></li>" +
|
|
3547
|
+
"<li aria-current=\"page\">Request an exchange</li>" +
|
|
3548
|
+
"</ol></nav>" +
|
|
3549
|
+
"<h1 class=\"return-form-page__title\">Request an exchange</h1>" +
|
|
3550
|
+
"<p class=\"return-form-page__order\">Order <code>" + esc(order.id) + "</code></p>" +
|
|
3551
|
+
notice +
|
|
3552
|
+
"<form class=\"return-form form-stack\" method=\"post\" action=\"/account/orders/" + esc(order.id) + "/exchange\">" +
|
|
3553
|
+
"<fieldset class=\"return-form__lines\"><legend>Which item?</legend>" +
|
|
3554
|
+
"<ul class=\"return-line-list\">" + lineRows + "</ul>" +
|
|
3555
|
+
"</fieldset>" +
|
|
3556
|
+
"<label class=\"form-field\"><span class=\"form-field__label\">Reason</span>" +
|
|
3557
|
+
"<select name=\"reason\" required>" + reasonOpts + "</select></label>" +
|
|
3558
|
+
"<button type=\"submit\" class=\"btn-primary\">Request exchange</button>" +
|
|
3559
|
+
"</form>" +
|
|
3560
|
+
"</section>";
|
|
3561
|
+
return _wrap({
|
|
3562
|
+
title: "Request an exchange",
|
|
3563
|
+
shop_name: opts.shop_name || "blamejs.shop",
|
|
3564
|
+
cart_count: opts.cart_count == null ? 0 : opts.cart_count,
|
|
3565
|
+
theme_css: opts.theme_css,
|
|
3566
|
+
body: body,
|
|
3567
|
+
});
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
// The signed-in customer's exchange list. `opts.exchanges` is the
|
|
3571
|
+
// ownership-scoped set (already filtered to this customer's orders); each
|
|
3572
|
+
// links to its status detail.
|
|
3573
|
+
function renderExchanges(opts) {
|
|
3574
|
+
var esc = b.template.escapeHtml;
|
|
3575
|
+
var exchanges = opts.exchanges || [];
|
|
3576
|
+
var rowsHtml = "";
|
|
3577
|
+
for (var i = 0; i < exchanges.length; i += 1) {
|
|
3578
|
+
var x = exchanges[i];
|
|
3579
|
+
var date = x.created_at ? new Date(Number(x.created_at)).toISOString().slice(0, 10) : "";
|
|
3580
|
+
rowsHtml +=
|
|
3581
|
+
"<li class=\"return-card\">" +
|
|
3582
|
+
"<div class=\"return-card__head\">" +
|
|
3583
|
+
"<a class=\"return-card__rma\" href=\"/account/exchanges/" + esc(String(x.id)) + "\"><code>" + esc(String(x.id).slice(0, 8)) + "</code></a>" +
|
|
3584
|
+
_exchangeStatusBadge(x.status) +
|
|
3585
|
+
"</div>" +
|
|
3586
|
+
"<p class=\"return-card__meta\">" +
|
|
3587
|
+
esc(String(x.return_sku || "")) + " → " + esc(String(x.replacement_sku || "")) +
|
|
3588
|
+
(date ? " · <time datetime=\"" + esc(date) + "\">" + esc(date) + "</time>" : "") +
|
|
3589
|
+
"</p>" +
|
|
3590
|
+
(x.status === "rejected" && x.reject_reason ? "<p class=\"return-card__reject\">" + esc(String(x.reject_reason)) + "</p>" : "") +
|
|
3591
|
+
"</li>";
|
|
3592
|
+
}
|
|
3593
|
+
var inner = rowsHtml
|
|
3594
|
+
? "<ul class=\"return-list\">" + rowsHtml + "</ul>"
|
|
3595
|
+
: "<div class=\"account-empty\">" +
|
|
3596
|
+
"<p class=\"account-empty__lede\">No exchanges yet. Start one from an order in your account.</p>" +
|
|
3597
|
+
"<a class=\"btn-secondary\" href=\"/account/orders\">View your orders →</a>" +
|
|
3598
|
+
"</div>";
|
|
3599
|
+
var notice = opts.requested
|
|
3600
|
+
? "<p class=\"form-notice form-notice--ok\" role=\"status\">Exchange request received — we'll review it and email you next steps.</p>"
|
|
3601
|
+
: "";
|
|
3602
|
+
var body =
|
|
3603
|
+
"<section class=\"account-returns\">" +
|
|
3604
|
+
"<nav class=\"breadcrumb\" aria-label=\"Breadcrumb\"><ol>" +
|
|
3605
|
+
"<li><a href=\"/account\">Account</a></li>" +
|
|
3606
|
+
"<li aria-current=\"page\">Exchanges</li>" +
|
|
3607
|
+
"</ol></nav>" +
|
|
3608
|
+
"<h1 class=\"account-returns__title\">Exchanges</h1>" +
|
|
3609
|
+
notice +
|
|
3610
|
+
inner +
|
|
3611
|
+
"</section>";
|
|
3612
|
+
return _wrap({
|
|
3613
|
+
title: "Exchanges",
|
|
3614
|
+
shop_name: opts.shop_name || "blamejs.shop",
|
|
3615
|
+
cart_count: opts.cart_count == null ? 0 : opts.cart_count,
|
|
3616
|
+
theme_css: opts.theme_css,
|
|
3617
|
+
body: body,
|
|
3618
|
+
});
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
// One exchange's status detail. Ownership-scoped (the route only renders
|
|
3622
|
+
// this for an exchange whose parent order belongs to the session
|
|
3623
|
+
// customer). Surfaces the swap, the reason, the live status, the
|
|
3624
|
+
// rejection reason (if declined), and the outbound tracking number once
|
|
3625
|
+
// the replacement has shipped.
|
|
3626
|
+
function renderExchangeDetail(opts) {
|
|
3627
|
+
var esc = b.template.escapeHtml;
|
|
3628
|
+
var x = opts.exchange;
|
|
3629
|
+
var date = x.created_at ? new Date(Number(x.created_at)).toISOString().slice(0, 10) : "";
|
|
3630
|
+
var rows = "";
|
|
3631
|
+
function _row(label, value) {
|
|
3632
|
+
return "<p class=\"return-card__meta\"><span class=\"form-field__label\">" + esc(label) + "</span> " +
|
|
3633
|
+
(value ? esc(String(value)) : "—") + "</p>";
|
|
3634
|
+
}
|
|
3635
|
+
rows += _row("Item returned", x.return_sku + " ×" + x.return_qty);
|
|
3636
|
+
rows += _row("Replacement", x.replacement_sku + " ×" + x.replacement_qty);
|
|
3637
|
+
rows += _row("Reason", x.reason);
|
|
3638
|
+
if (date) rows += _row("Requested", date);
|
|
3639
|
+
if (x.status === "rejected" && x.reject_reason) rows += _row("Why it was declined", x.reject_reason);
|
|
3640
|
+
if (x.tracking_number) {
|
|
3641
|
+
rows += _row("Replacement tracking", x.carrier ? (x.carrier + " · " + x.tracking_number) : x.tracking_number);
|
|
3642
|
+
}
|
|
3643
|
+
var body =
|
|
3644
|
+
"<section class=\"account-returns\">" +
|
|
3645
|
+
"<nav class=\"breadcrumb\" aria-label=\"Breadcrumb\"><ol>" +
|
|
3646
|
+
"<li><a href=\"/account\">Account</a></li>" +
|
|
3647
|
+
"<li><a href=\"/account/exchanges\">Exchanges</a></li>" +
|
|
3648
|
+
"<li aria-current=\"page\">" + esc(String(x.id).slice(0, 8)) + "</li>" +
|
|
3649
|
+
"</ol></nav>" +
|
|
3650
|
+
"<div class=\"return-card__head\">" +
|
|
3651
|
+
"<h1 class=\"account-returns__title\">Exchange <code>" + esc(String(x.id).slice(0, 8)) + "</code></h1>" +
|
|
3652
|
+
_exchangeStatusBadge(x.status) +
|
|
3653
|
+
"</div>" +
|
|
3654
|
+
"<div class=\"return-card\">" + rows + "</div>" +
|
|
3655
|
+
"</section>";
|
|
3656
|
+
return _wrap({
|
|
3657
|
+
title: "Exchange " + String(x.id).slice(0, 8),
|
|
3658
|
+
shop_name: opts.shop_name || "blamejs.shop",
|
|
3659
|
+
cart_count: opts.cart_count == null ? 0 : opts.cart_count,
|
|
3660
|
+
theme_css: opts.theme_css,
|
|
3661
|
+
body: body,
|
|
3662
|
+
});
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3465
3665
|
// Support ticket — the categories a shopper can pick when raising one,
|
|
3466
3666
|
// and the human label for each. The values mirror the support-tickets
|
|
3467
3667
|
// module's ALLOWED_CATEGORIES; the backend validates the submitted value
|
|
@@ -5255,6 +5455,17 @@ function _orderEligibleForReturn(status) {
|
|
|
5255
5455
|
status === "shipped" || status === "delivered";
|
|
5256
5456
|
}
|
|
5257
5457
|
|
|
5458
|
+
// An exchange (swap the item for a different size / colour / unit at the
|
|
5459
|
+
// same value) is offered on the same window as a return: a paid (or
|
|
5460
|
+
// further-along) order, never a still-pending (unpaid) one nor the
|
|
5461
|
+
// terminal off-ramps (cancelled / refunded). The customer ships the
|
|
5462
|
+
// original back AND receives a replacement — no refund — so the window
|
|
5463
|
+
// matches the goods being in the customer's hands.
|
|
5464
|
+
function _orderEligibleForExchange(status) {
|
|
5465
|
+
return status === "paid" || status === "fulfilling" ||
|
|
5466
|
+
status === "shipped" || status === "delivered";
|
|
5467
|
+
}
|
|
5468
|
+
|
|
5258
5469
|
// Reorder is offered for any order that represents a real purchase the
|
|
5259
5470
|
// customer might want to repeat — paid through delivered, plus the
|
|
5260
5471
|
// terminal refunded/cancelled states (a cancelled or refunded order is a
|
|
@@ -5378,6 +5589,10 @@ function _orderActionsBlock(o) {
|
|
|
5378
5589
|
btns.push(
|
|
5379
5590
|
"<a class=\"btn-secondary order-action\" href=\"/account/orders/" + esc(String(o.id)) + "/return\">Request a return</a>");
|
|
5380
5591
|
}
|
|
5592
|
+
if (_orderEligibleForExchange(o.status)) {
|
|
5593
|
+
btns.push(
|
|
5594
|
+
"<a class=\"btn-secondary order-action\" href=\"/account/orders/" + esc(String(o.id)) + "/exchange\">Request an exchange</a>");
|
|
5595
|
+
}
|
|
5381
5596
|
if (_orderEligibleForCancel(o.status)) {
|
|
5382
5597
|
btns.push(
|
|
5383
5598
|
"<form class=\"order-action\" method=\"post\" action=\"/orders/" + esc(String(o.id)) + "/cancel\">" +
|
|
@@ -6641,6 +6856,7 @@ var ACCOUNT_DASH_PAGE =
|
|
|
6641
6856
|
" <a class=\"btn-secondary\" href=\"/account/recently-viewed\">Recently viewed</a>\n" +
|
|
6642
6857
|
" <a class=\"btn-secondary\" href=\"/account/addresses\">Addresses</a>\n" +
|
|
6643
6858
|
" <a class=\"btn-secondary\" href=\"/account/returns\">Returns</a>\n" +
|
|
6859
|
+
" <a class=\"btn-secondary\" href=\"/account/exchanges\">Exchanges</a>\n" +
|
|
6644
6860
|
" <a class=\"btn-secondary\" href=\"/account/support\">Support</a>\n" +
|
|
6645
6861
|
" <a class=\"btn-secondary\" href=\"/account/loyalty\">Rewards</a>\n" +
|
|
6646
6862
|
" <a class=\"btn-secondary\" href=\"/account/credit\">Store credit</a>\n" +
|
|
@@ -11769,6 +11985,240 @@ function mount(router, deps) {
|
|
|
11769
11985
|
});
|
|
11770
11986
|
}
|
|
11771
11987
|
|
|
11988
|
+
// Self-serve exchanges — a customer requests a same-value item SWAP
|
|
11989
|
+
// (different size / colour / unit) against one of their own orders and
|
|
11990
|
+
// tracks its status; operators action it via the admin /admin/exchanges
|
|
11991
|
+
// queue. Distinct from returns (which end in a refund): an exchange
|
|
11992
|
+
// ships the original back AND a replacement out, no money refunded.
|
|
11993
|
+
//
|
|
11994
|
+
// Ownership / IDOR: an order_exchanges row carries `order_id` but NOT
|
|
11995
|
+
// `customer_id` — the customer→order linkage lives on the order. So
|
|
11996
|
+
// every per-exchange / per-order route here loads the parent order and
|
|
11997
|
+
// refuses (clean 404) unless `order.customer_id === auth.customer_id`,
|
|
11998
|
+
// exactly like the returns + support gates. The exchange primitive
|
|
11999
|
+
// moves a row by id alone, so the route owns the ownership decision.
|
|
12000
|
+
if (deps.orderExchanges && deps.order) {
|
|
12001
|
+
function _exchangeAuth(req, res) {
|
|
12002
|
+
var auth;
|
|
12003
|
+
try { auth = _currentCustomer(req); }
|
|
12004
|
+
catch (e) {
|
|
12005
|
+
if (e && e.code === "vault/not-initialized") { _serviceUnavailable(res, "auth not configured"); return null; }
|
|
12006
|
+
throw e;
|
|
12007
|
+
}
|
|
12008
|
+
if (!auth) {
|
|
12009
|
+
res.status(303); res.setHeader && res.setHeader("location", "/account/login");
|
|
12010
|
+
res.end ? res.end() : res.send("");
|
|
12011
|
+
return null;
|
|
12012
|
+
}
|
|
12013
|
+
return auth;
|
|
12014
|
+
}
|
|
12015
|
+
|
|
12016
|
+
// Load the order named in :order_id and confirm it belongs to the
|
|
12017
|
+
// signed-in customer. A malformed id (guardUuid TypeError), a missing
|
|
12018
|
+
// order, or someone else's order all return 404 — never a 500, never
|
|
12019
|
+
// a leak. This is the ownership gate the request form + POST funnel
|
|
12020
|
+
// through.
|
|
12021
|
+
async function _ownedOrderForExchange(req, res, auth) {
|
|
12022
|
+
var order;
|
|
12023
|
+
try { order = await deps.order.get(req.params && req.params.order_id); }
|
|
12024
|
+
catch (e) {
|
|
12025
|
+
if (e instanceof TypeError) { _send(res, 404, renderNotFound({ shop_name: shopName, theme: theme })); return null; }
|
|
12026
|
+
throw e;
|
|
12027
|
+
}
|
|
12028
|
+
if (!order || order.customer_id !== auth.customer_id) {
|
|
12029
|
+
_send(res, 404, renderNotFound({ shop_name: shopName, theme: theme }));
|
|
12030
|
+
return null;
|
|
12031
|
+
}
|
|
12032
|
+
return order;
|
|
12033
|
+
}
|
|
12034
|
+
|
|
12035
|
+
// Load the exchange named in :id, then load its parent order and
|
|
12036
|
+
// confirm THAT order belongs to the signed-in customer. The exchange
|
|
12037
|
+
// row has no customer_id, so ownership is asserted transitively
|
|
12038
|
+
// through the order. A malformed id (TypeError), an unknown exchange,
|
|
12039
|
+
// an exchange whose order is gone, or an exchange owned by someone
|
|
12040
|
+
// else ALL return 404 — never a 500, never a cross-customer reveal.
|
|
12041
|
+
async function _ownedExchange(req, res, auth) {
|
|
12042
|
+
var exchange;
|
|
12043
|
+
try { exchange = await deps.orderExchanges.getExchange(req.params && req.params.id); }
|
|
12044
|
+
catch (e) {
|
|
12045
|
+
if (e instanceof TypeError) { _send(res, 404, renderNotFound({ shop_name: shopName, theme: theme })); return null; }
|
|
12046
|
+
throw e;
|
|
12047
|
+
}
|
|
12048
|
+
if (!exchange) { _send(res, 404, renderNotFound({ shop_name: shopName, theme: theme })); return null; }
|
|
12049
|
+
var order;
|
|
12050
|
+
try { order = await deps.order.get(exchange.order_id); }
|
|
12051
|
+
catch (e) {
|
|
12052
|
+
if (e instanceof TypeError) { _send(res, 404, renderNotFound({ shop_name: shopName, theme: theme })); return null; }
|
|
12053
|
+
throw e;
|
|
12054
|
+
}
|
|
12055
|
+
if (!order || order.customer_id !== auth.customer_id) {
|
|
12056
|
+
_send(res, 404, renderNotFound({ shop_name: shopName, theme: theme }));
|
|
12057
|
+
return null;
|
|
12058
|
+
}
|
|
12059
|
+
return exchange;
|
|
12060
|
+
}
|
|
12061
|
+
|
|
12062
|
+
// Resolve the sibling variants a line's product can be swapped to:
|
|
12063
|
+
// every active variant of the same product, labelled by its option
|
|
12064
|
+
// values (falling back to the SKU). Best-effort — absent the catalog
|
|
12065
|
+
// handle (or a read failure) the line offers a same-SKU swap.
|
|
12066
|
+
async function _replacementOptions(line) {
|
|
12067
|
+
if (!deps.catalog || !line || !line.variant_id) return [];
|
|
12068
|
+
try {
|
|
12069
|
+
var v = await deps.catalog.variants.get(line.variant_id);
|
|
12070
|
+
if (!v) return [];
|
|
12071
|
+
var siblings = await deps.catalog.variants.listForProduct(v.product_id);
|
|
12072
|
+
return (siblings || []).map(function (s) {
|
|
12073
|
+
var optLabel = s.options
|
|
12074
|
+
? Object.keys(s.options).map(function (k) { return s.options[k]; }).join(" / ")
|
|
12075
|
+
: "";
|
|
12076
|
+
var label = (optLabel ? optLabel + " — " : "") + s.sku;
|
|
12077
|
+
return { id: s.id, sku: s.sku, label: label };
|
|
12078
|
+
});
|
|
12079
|
+
} catch (_e) { return []; }
|
|
12080
|
+
}
|
|
12081
|
+
|
|
12082
|
+
// The customer's own exchange list, scoped to their orders. The
|
|
12083
|
+
// primitive resolves the customer→order linkage through the injected
|
|
12084
|
+
// order handle, so a foreign order's exchange never appears here.
|
|
12085
|
+
router.get("/account/exchanges", async function (req, res) {
|
|
12086
|
+
var auth = _exchangeAuth(req, res); if (!auth) return;
|
|
12087
|
+
var exchanges = [];
|
|
12088
|
+
try { exchanges = await deps.orderExchanges.exchangesForCustomer(auth.customer_id, { limit: 100 }); }
|
|
12089
|
+
catch (e) { if (!(e instanceof TypeError)) throw e; exchanges = []; }
|
|
12090
|
+
var cartCount = await _cartCountForReq(req);
|
|
12091
|
+
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
12092
|
+
_send(res, 200, renderExchanges({
|
|
12093
|
+
exchanges: exchanges,
|
|
12094
|
+
requested: url && url.searchParams.get("ok") === "1",
|
|
12095
|
+
shop_name: shopName,
|
|
12096
|
+
cart_count: cartCount,
|
|
12097
|
+
}));
|
|
12098
|
+
});
|
|
12099
|
+
|
|
12100
|
+
// One exchange's status detail. Ownership-scoped through the parent
|
|
12101
|
+
// order (foreign / unknown → 404).
|
|
12102
|
+
router.get("/account/exchanges/:id", async function (req, res) {
|
|
12103
|
+
var auth = _exchangeAuth(req, res); if (!auth) return;
|
|
12104
|
+
var exchange = await _ownedExchange(req, res, auth); if (!exchange) return;
|
|
12105
|
+
var cartCount = await _cartCountForReq(req);
|
|
12106
|
+
_send(res, 200, renderExchangeDetail({ exchange: exchange, shop_name: shopName, cart_count: cartCount }));
|
|
12107
|
+
});
|
|
12108
|
+
|
|
12109
|
+
// The exchange-request form for one of the customer's own orders,
|
|
12110
|
+
// gated on the same eligibility window as a return.
|
|
12111
|
+
router.get("/account/orders/:order_id/exchange", async function (req, res) {
|
|
12112
|
+
var auth = _exchangeAuth(req, res); if (!auth) return;
|
|
12113
|
+
var order = await _ownedOrderForExchange(req, res, auth); if (!order) return;
|
|
12114
|
+
var cartCount = await _cartCountForReq(req);
|
|
12115
|
+
if (!_orderEligibleForExchange(order.status)) {
|
|
12116
|
+
return _send(res, 400, renderExchangeForm({
|
|
12117
|
+
order: order, lines: [], notice: "This order isn't eligible for an exchange.",
|
|
12118
|
+
shop_name: shopName, cart_count: cartCount,
|
|
12119
|
+
}));
|
|
12120
|
+
}
|
|
12121
|
+
var lines = [];
|
|
12122
|
+
var orderLines = order.lines || [];
|
|
12123
|
+
for (var i = 0; i < orderLines.length; i += 1) {
|
|
12124
|
+
var ol = orderLines[i];
|
|
12125
|
+
lines.push(Object.assign({}, ol, { replacement_options: await _replacementOptions(ol) }));
|
|
12126
|
+
}
|
|
12127
|
+
_send(res, 200, renderExchangeForm({ order: order, lines: lines, shop_name: shopName, cart_count: cartCount }));
|
|
12128
|
+
});
|
|
12129
|
+
|
|
12130
|
+
router.post("/account/orders/:order_id/exchange", async function (req, res) {
|
|
12131
|
+
var auth = _exchangeAuth(req, res); if (!auth) return;
|
|
12132
|
+
var order = await _ownedOrderForExchange(req, res, auth); if (!order) return;
|
|
12133
|
+
var body = req.body || {};
|
|
12134
|
+
var cartCount = await _cartCountForReq(req);
|
|
12135
|
+
|
|
12136
|
+
// Re-decorate the lines so a failed POST re-renders the same form
|
|
12137
|
+
// (with the replacement pickers intact) rather than a bare page.
|
|
12138
|
+
async function _decoratedLines() {
|
|
12139
|
+
var out = [];
|
|
12140
|
+
var ols = order.lines || [];
|
|
12141
|
+
for (var i = 0; i < ols.length; i += 1) {
|
|
12142
|
+
out.push(Object.assign({}, ols[i], { replacement_options: await _replacementOptions(ols[i]) }));
|
|
12143
|
+
}
|
|
12144
|
+
return out;
|
|
12145
|
+
}
|
|
12146
|
+
function _badForm(notice, status) {
|
|
12147
|
+
return _decoratedLines().then(function (lines) {
|
|
12148
|
+
return _send(res, status || 400, renderExchangeForm({
|
|
12149
|
+
order: order, lines: lines, notice: notice,
|
|
12150
|
+
shop_name: shopName, cart_count: cartCount,
|
|
12151
|
+
}));
|
|
12152
|
+
});
|
|
12153
|
+
}
|
|
12154
|
+
|
|
12155
|
+
if (!_orderEligibleForExchange(order.status)) {
|
|
12156
|
+
return _badForm("This order isn't eligible for an exchange.");
|
|
12157
|
+
}
|
|
12158
|
+
|
|
12159
|
+
// Resolve the picked line from the order's OWN lines (authoritative
|
|
12160
|
+
// sku/qty) — never trust a client-supplied sku. The radio carries
|
|
12161
|
+
// the order_line id.
|
|
12162
|
+
var orderLines = order.lines || [];
|
|
12163
|
+
var picked = null;
|
|
12164
|
+
for (var i = 0; i < orderLines.length; i += 1) {
|
|
12165
|
+
if (orderLines[i].id === body.line_id) { picked = orderLines[i]; break; }
|
|
12166
|
+
}
|
|
12167
|
+
if (!picked) return _badForm("Pick the item you'd like to exchange.");
|
|
12168
|
+
|
|
12169
|
+
var wantedQty = parseInt(body["qty_" + picked.id], 10);
|
|
12170
|
+
var maxQty = Number(picked.qty) || 1;
|
|
12171
|
+
var qty = Number.isFinite(wantedQty) && wantedQty >= 1 && wantedQty <= maxQty ? wantedQty : maxQty;
|
|
12172
|
+
|
|
12173
|
+
// The replacement variant: the chosen sibling variant of the same
|
|
12174
|
+
// product. The select carries the variant id; resolve it back to a
|
|
12175
|
+
// sku through the catalog (never trust a client sku). Absent a
|
|
12176
|
+
// selection (a same-SKU swap — replace a defective unit) the
|
|
12177
|
+
// replacement defaults to the line's own sku/variant.
|
|
12178
|
+
var replacementSku = picked.sku;
|
|
12179
|
+
var replacementVariantId = picked.variant_id;
|
|
12180
|
+
var chosen = body["replacement_" + picked.id];
|
|
12181
|
+
if (chosen && deps.catalog) {
|
|
12182
|
+
// The replacement MUST be one of the sibling variants offered for
|
|
12183
|
+
// the purchased line's product. Validate the chosen id against the
|
|
12184
|
+
// same option set the form was built from (_replacementOptions) —
|
|
12185
|
+
// resolving it through a bare variants.get would accept ANY catalog
|
|
12186
|
+
// variant, letting a forged replacement_<id> swap a cheap item for
|
|
12187
|
+
// any variant in the catalog. The option carries the catalog-
|
|
12188
|
+
// resolved sku/id, so a client sku is never trusted.
|
|
12189
|
+
var options = await _replacementOptions(picked);
|
|
12190
|
+
var match = null;
|
|
12191
|
+
for (var oi = 0; oi < options.length; oi += 1) {
|
|
12192
|
+
if (options[oi].id === chosen) { match = options[oi]; break; }
|
|
12193
|
+
}
|
|
12194
|
+
if (!match) return _badForm("That replacement variant isn't available — pick another.");
|
|
12195
|
+
replacementSku = match.sku;
|
|
12196
|
+
replacementVariantId = match.id;
|
|
12197
|
+
}
|
|
12198
|
+
|
|
12199
|
+
try {
|
|
12200
|
+
await deps.orderExchanges.requestExchange({
|
|
12201
|
+
order_id: order.id,
|
|
12202
|
+
line_id: picked.id,
|
|
12203
|
+
return_sku: picked.sku,
|
|
12204
|
+
return_qty: qty,
|
|
12205
|
+
replacement_sku: replacementSku,
|
|
12206
|
+
replacement_variant_id: replacementVariantId,
|
|
12207
|
+
replacement_qty: qty,
|
|
12208
|
+
reason: body.reason,
|
|
12209
|
+
});
|
|
12210
|
+
} catch (e) {
|
|
12211
|
+
if (e instanceof TypeError) {
|
|
12212
|
+
return _badForm((e && e.message || "").replace(/^order-exchanges[.:]\s*/, "") || "Please check your exchange request.");
|
|
12213
|
+
}
|
|
12214
|
+
throw e;
|
|
12215
|
+
}
|
|
12216
|
+
res.status(303);
|
|
12217
|
+
res.setHeader && res.setHeader("location", "/account/exchanges?ok=1");
|
|
12218
|
+
return res.end ? res.end() : res.send("");
|
|
12219
|
+
});
|
|
12220
|
+
}
|
|
12221
|
+
|
|
11772
12222
|
// Support tickets — the signed-in customer raises a ticket, lists
|
|
11773
12223
|
// their own, reads a thread, and replies. EVERY route is login-gated
|
|
11774
12224
|
// AND scoped to the session customer's id: the support primitive
|
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.10",
|
|
7
|
+
"tag": "v0.14.10",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"server": "lib/vendor/blamejs/"
|
|
14
14
|
},
|
|
15
15
|
"bundler": "shallow git clone of release tag from github.com/blamejs/blamejs",
|
|
16
|
-
"bundledAt": "2026-05-
|
|
16
|
+
"bundledAt": "2026-05-31"
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -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.10 (2026-05-31) — **Full-text-search token hashes move to a keyed MAC; existing mail-store search indexes rebuild automatically on upgrade.** The mail-store full-text-search index hashed its tokens with a hand-rolled salted-SHA3 derived hash. It now routes through the framework's sealed-column hashing primitive in keyed mode (HMAC-SHAKE256 off the per-deployment MAC key), so a search-index token hash is unforgeable and un-correlatable across deployments without that key — the same posture the sealed-column lookup hashes already use. Because the keyed hash changes the stored token values, a mail-store opened after upgrade detects its index as old-format and rebuilds it once from the sealed message rows. The rebuild runs under a format marker: the index is marked `rebuilding` before it is cleared and only marked current after every row is re-hashed inside an explicit transaction, and search falls back to its cursor path (rather than returning partial hits) whenever the marker is not current — so an interrupted rebuild leaves the old index intact and queryable and retries on the next open, never serving a half-built index. A new `b.cryptoField.computeNamespacedHash` primitive backs the keyed hashing for callers that hash outside the registered-column path. **Added:** *`b.cryptoField.computeNamespacedHash`* — A mode-aware namespaced hash for indexed-lookup callers that hash a value outside the registered-column derived-hash path. `computeNamespacedHash(ns, value, { mode, truncateBytes })` routes through the same engine as `computeDerived` — `salted-sha3` (default) or the keyed `hmac-shake256` — with optional hex truncation. The mail-store full-text index is the first consumer. **Changed:** *Mail-store full-text index rehashes to a keyed MAC on upgrade* — The full-text-search token hash now uses `b.cryptoField.computeNamespacedHash` in `hmac-shake256` mode instead of a hand-rolled salted-SHA3. The first time a store is opened after upgrade, its index is detected as old-format and rebuilt once from the sealed message rows; subsequent opens are no-ops. Search is unaffected once the rebuild completes. The rebuild requires the vault to be initialized and fails closed (a clear error) at construction if it is not, rather than leaving a stale searchable index. **Security:** *Keyed, un-correlatable full-text-search token hashes* — A search-index token hash is now a keyed MAC over a per-deployment key, not a static-salted digest — it cannot be forged or correlated across deployments without that key, closing the low-entropy-token correlation gap on the search index. The index remains unrecoverable from a database dump alone, as before. **Detectors:** *Hand-rolled lookup-hash check covers the split form* — The check that requires sealed-column lookup hashes to compose the framework primitive now also catches the across-lines hand-roll (`var salt = getDerivedHashSalt(); var hex = salt.toString(...); sha3(hex + ns + value)`), not only the single-expression form, so the bypass that the mail-store index used can't reappear. **Migration:** *Automatic, one-time full-text index rebuild* — No operator action is required: the rebuild runs automatically and idempotently on first open after upgrade, atomically and crash-safe (an interrupted rebuild keeps the old index and retries). The only requirement is that the vault is initialized before the mail-store is constructed. One caveat for shared stores: do not run a pre-upgrade and post-upgrade node against the same backend file concurrently across this format change — the old node would write old-format hashes the new node cannot match. Roll the deployment fully across the upgrade. This re-open condition is lifted once all nodes are on 0.14.10 or later.
|
|
12
|
+
|
|
11
13
|
- v0.14.9 (2026-05-30) — **Corrects EU AI Act doc paths that named an uncallable namespace, plus source-comment hygiene and two new codebase checks.** A documentation fix and internal hygiene. The `@primitive` / `@signature` / `@example` blocks for the EU AI Act fundamental-rights-impact-assessment and GPAI training-data-summary helpers advertised `b.complianceAiAct.*`, which is undefined — the callable path is `b.compliance.aiAct.*` — so an operator copying the documented call got `undefined is not a function`. The documented paths now match the real surface. Alongside that: a duplicate parser entry in a doc block is removed, version stamps embedded in section-divider comments are stripped, and two codebase checks are added — one that fails the build when a `@primitive` block documents a wholly-unresolvable namespace (the gap that hid the AI Act paths), and one that flags a version stamp left inside a section divider. No exported API, error code, wire format, or runtime behaviour changes. **Changed:** *Source-comment hygiene* — Removed a duplicate `env` entry from the parsers `@module` doc block, and stripped internal version stamps (`vX.Y.Z`) from `// ---- ... ----` section-divider comments across several files, keeping the descriptive label. Comment-only; no behaviour change. **Fixed:** *EU AI Act helper documentation named an uncallable path* — `b.compliance.aiAct.fundamentalRightsImpactAssessment` and `b.compliance.aiAct.gpai.trainingDataSummary` were documented as `b.complianceAiAct.*` in their `@primitive` / `@signature` / `@example` blocks (and one returned reference string). `b.complianceAiAct` is undefined, so the documented call failed; the documented paths now match the callable surface. **Detectors:** *`@primitive` reachability covers wrong-namespace paths* — The reachability check previously only flagged a missing leaf on a resolved namespace; a `@primitive` whose entire dotted prefix is unresolvable (the shape that hid the AI Act doc paths) was silently skipped. It now walks each prefix segment and fails the build on any unresolvable one, while preserving the factory-instance-shorthand exemption. · *Version-stamp-in-divider check* — A new check flags a version stamp (`vX.Y.Z`) left immediately after a section divider's dashes (`// ---- vX.Y.Z ...`) — internal release vocabulary that does not belong in shipped source comments — without matching legitimate `@since` tags or prose version references.
|
|
12
14
|
|
|
13
15
|
- v0.14.8 (2026-05-30) — **Source-comment and codebase-check hygiene, plus a new require-block alignment check; no API or behaviour changes.** Internal lint and comment cleanup with no operator-facing surface change. Several codebase-check comments and one stub helper name that described behaviour the check no longer has are corrected; an unused lint-suppression class and a set of stale duplicate-cluster qualifiers (functions that were since renamed or extracted) are pruned or re-pointed. Fifty-nine `// allow:` markers that named the byte-size or time-literal check on values those checks no longer flag are removed, and twenty self-negating rationales on markers the time check genuinely fires on are rewritten to say why the value coincidentally matches. A new codebase check holds top-of-file require blocks to consistent `=` column alignment, with the files that currently carry drift listed as a migration allowlist. No exported API, error code, wire format, or runtime behaviour changes. **Changed:** *Lint-suppression and codebase-check comment cleanup* — Corrected codebase-check comments that overstated their check's scope (a duplicate-code threshold described as three files when the advisory threshold is two, a narrowed byte-literal check carrying its pre-narrowing description, and a deferred-scan helper named as though it enforced a guarantee it does not yet provide). Removed an unused lint-suppression class and its one dead in-code marker, and pruned or re-pointed stale duplicate-cluster qualifiers that named functions since renamed or extracted into shared helpers. Removed fifty-nine `// allow:` markers that suppressed nothing, and rewrote twenty self-negating marker rationales (which read "not seconds" while sitting on a value the time check fires on) to explain the coincidental match. Source-comment and test hygiene only. **Detectors:** *Require-block `=` alignment check* — A new codebase check flags a top-of-file require block that mixes its `=` column alignment — a fittable line whose `=` drifts off the column the rest of the block shares. Compact single-space blocks are exempt (only blocks that declare alignment intent are checked), as are destructures and long names physically too wide to reach the column, and blank- or comment-separated tiers align independently. The files that currently carry such drift are an explicit migration allowlist, reflowed over time; new code is held to the rule.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"frameworkVersion": "0.14.
|
|
4
|
-
"createdAt": "2026-05-
|
|
3
|
+
"frameworkVersion": "0.14.10",
|
|
4
|
+
"createdAt": "2026-05-31T10:40:06.680Z",
|
|
5
5
|
"exports": {
|
|
6
6
|
"a2a": {
|
|
7
7
|
"type": "object",
|
|
@@ -14132,6 +14132,10 @@
|
|
|
14132
14132
|
"type": "function",
|
|
14133
14133
|
"arity": 3
|
|
14134
14134
|
},
|
|
14135
|
+
"computeNamespacedHash": {
|
|
14136
|
+
"type": "function",
|
|
14137
|
+
"arity": 3
|
|
14138
|
+
},
|
|
14135
14139
|
"declareColumnResidency": {
|
|
14136
14140
|
"type": "function",
|
|
14137
14141
|
"arity": 2
|
|
@@ -40497,6 +40501,14 @@
|
|
|
40497
40501
|
}
|
|
40498
40502
|
}
|
|
40499
40503
|
},
|
|
40504
|
+
"FTS_FORMAT_VERSION": {
|
|
40505
|
+
"type": "primitive",
|
|
40506
|
+
"valueType": "number"
|
|
40507
|
+
},
|
|
40508
|
+
"FTS_HASH_BYTES": {
|
|
40509
|
+
"type": "primitive",
|
|
40510
|
+
"valueType": "number"
|
|
40511
|
+
},
|
|
40500
40512
|
"MAX_TOKENS_PER_FIELD": {
|
|
40501
40513
|
"type": "primitive",
|
|
40502
40514
|
"valueType": "number"
|
|
@@ -235,6 +235,74 @@ function _computeDerivedHash(spec, tableMode, ns, normalized) {
|
|
|
235
235
|
return sha3Hash(vault.getDerivedHashSalt().toString("hex") + ns + normalized);
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* @primitive b.cryptoField.computeNamespacedHash
|
|
240
|
+
* @signature b.cryptoField.computeNamespacedHash(ns, value, opts?)
|
|
241
|
+
* @since 0.14.10
|
|
242
|
+
* @compliance gdpr, hipaa
|
|
243
|
+
* @related b.cryptoField.computeDerived, b.cryptoField.lookupHash
|
|
244
|
+
*
|
|
245
|
+
* Computes a namespaced indexed-lookup digest of `value` for a
|
|
246
|
+
* pseudo-field that is NOT backed by a registered derived-hash column
|
|
247
|
+
* (e.g. the sealed-token FTS index in `b.mailStore.fts`). The caller
|
|
248
|
+
* supplies the full namespace string directly — there is no schema
|
|
249
|
+
* lookup — so the same keyed/salted hash machinery that protects
|
|
250
|
+
* registered derived hashes also covers ad-hoc indexed tokens. This is
|
|
251
|
+
* the canonical entry point: hand-rolling
|
|
252
|
+
* `sha3Hash(vault.getDerivedHashSalt() + ns + value)` at a call site
|
|
253
|
+
* bypasses the keyed-MAC mode (`hmac-shake256` off
|
|
254
|
+
* `vault.getDerivedHashMacKey`) and the per-deployment salt policy.
|
|
255
|
+
*
|
|
256
|
+
* `opts.mode` selects the digest:
|
|
257
|
+
* - `"salted-sha3"` (default): SHA3-512 over `<salt-hex> + ns + value`
|
|
258
|
+
* (deterministic per deployment; byte-identical to the legacy
|
|
259
|
+
* hand-rolled scheme).
|
|
260
|
+
* - `"hmac-shake256"`: SHAKE256(`<vault MAC key> || ns + value`) — a
|
|
261
|
+
* keyed MAC so an attacker who recovers the salt alone cannot
|
|
262
|
+
* correlate two low-entropy plaintexts.
|
|
263
|
+
*
|
|
264
|
+
* `opts.truncateBytes` truncates the hex digest to that many BYTES
|
|
265
|
+
* (the hex string is sliced to `truncateBytes * 2` characters). Throws
|
|
266
|
+
* (config-time / entry-point tier) on an unknown `mode` or a
|
|
267
|
+
* non-positive-integer `truncateBytes` so an operator catches the typo
|
|
268
|
+
* at boot rather than silently indexing under a malformed digest.
|
|
269
|
+
*
|
|
270
|
+
* @opts
|
|
271
|
+
* mode: string, // "salted-sha3" (default) | "hmac-shake256"
|
|
272
|
+
* truncateBytes: number, // optional; positive integer byte width to slice to
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* var ns = "bj-mail_messages-body:fts:";
|
|
276
|
+
* var h = b.cryptoField.computeNamespacedHash(ns, "kubernetes", {
|
|
277
|
+
* mode: "hmac-shake256", truncateBytes: 8
|
|
278
|
+
* });
|
|
279
|
+
* /^[0-9a-f]{16}$/.test(h); // → true
|
|
280
|
+
*
|
|
281
|
+
* // Default mode is byte-identical to the legacy salted-sha3 hash.
|
|
282
|
+
* b.cryptoField.computeNamespacedHash(ns, "kubernetes").length; // → 128
|
|
283
|
+
*/
|
|
284
|
+
function computeNamespacedHash(ns, value, opts) {
|
|
285
|
+
opts = opts || {};
|
|
286
|
+
var mode = opts.mode || "salted-sha3";
|
|
287
|
+
if (mode !== "salted-sha3" && mode !== "hmac-shake256") {
|
|
288
|
+
throw new Error("computeNamespacedHash: opts.mode must be 'salted-sha3' " +
|
|
289
|
+
"(default) or 'hmac-shake256', got " + JSON.stringify(mode));
|
|
290
|
+
}
|
|
291
|
+
var truncateBytes = opts.truncateBytes;
|
|
292
|
+
if (truncateBytes !== undefined) {
|
|
293
|
+
if (typeof truncateBytes !== "number" || !isFinite(truncateBytes) ||
|
|
294
|
+
truncateBytes <= 0 || Math.floor(truncateBytes) !== truncateBytes) {
|
|
295
|
+
throw new Error("computeNamespacedHash: opts.truncateBytes must be a " +
|
|
296
|
+
"positive integer (bytes), got " + JSON.stringify(truncateBytes));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
var hex = _computeDerivedHash({ mode: mode }, mode, ns, String(value));
|
|
300
|
+
if (truncateBytes !== undefined) {
|
|
301
|
+
return hex.slice(0, truncateBytes * 2);
|
|
302
|
+
}
|
|
303
|
+
return hex;
|
|
304
|
+
}
|
|
305
|
+
|
|
238
306
|
/**
|
|
239
307
|
* @primitive b.cryptoField.getSchema
|
|
240
308
|
* @signature b.cryptoField.getSchema(table)
|
|
@@ -1046,6 +1114,7 @@ module.exports = {
|
|
|
1046
1114
|
applyPosture: applyPosture,
|
|
1047
1115
|
getActivePosture: getActivePosture,
|
|
1048
1116
|
computeDerived: computeDerived,
|
|
1117
|
+
computeNamespacedHash: computeNamespacedHash,
|
|
1049
1118
|
lookupHash: lookupHash,
|
|
1050
1119
|
clearForTest: clearForTest,
|
|
1051
1120
|
declareColumnResidency: declareColumnResidency,
|