@blamejs/blamejs-shop 0.3.11 → 0.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/admin.js +157 -117
  3. package/lib/asset-manifest.json +1 -1
  4. package/lib/currency-rounding.js +2 -14
  5. package/lib/index.js +1 -0
  6. package/lib/text-guard.js +227 -0
  7. package/lib/vendor/MANIFEST.json +2 -2
  8. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  9. package/lib/vendor/blamejs/README.md +3 -2
  10. package/lib/vendor/blamejs/SECURITY.md +3 -0
  11. package/lib/vendor/blamejs/api-snapshot.json +14 -2
  12. package/lib/vendor/blamejs/lib/agent-event-bus.js +4 -4
  13. package/lib/vendor/blamejs/lib/agent-idempotency.js +6 -6
  14. package/lib/vendor/blamejs/lib/agent-orchestrator.js +9 -9
  15. package/lib/vendor/blamejs/lib/agent-posture-chain.js +10 -10
  16. package/lib/vendor/blamejs/lib/agent-saga.js +6 -7
  17. package/lib/vendor/blamejs/lib/agent-snapshot.js +8 -8
  18. package/lib/vendor/blamejs/lib/agent-stream.js +3 -3
  19. package/lib/vendor/blamejs/lib/agent-tenant.js +4 -4
  20. package/lib/vendor/blamejs/lib/agent-trace.js +5 -5
  21. package/lib/vendor/blamejs/lib/ai-disclosure.js +3 -3
  22. package/lib/vendor/blamejs/lib/app.js +2 -2
  23. package/lib/vendor/blamejs/lib/archive-read.js +1 -1
  24. package/lib/vendor/blamejs/lib/archive-tar-read.js +1 -1
  25. package/lib/vendor/blamejs/lib/archive-wrap.js +5 -5
  26. package/lib/vendor/blamejs/lib/audit-tools.js +65 -5
  27. package/lib/vendor/blamejs/lib/audit.js +2 -2
  28. package/lib/vendor/blamejs/lib/auth/ciba.js +1 -1
  29. package/lib/vendor/blamejs/lib/auth/dpop.js +1 -1
  30. package/lib/vendor/blamejs/lib/auth/fal.js +1 -1
  31. package/lib/vendor/blamejs/lib/auth/fido-mds3.js +2 -3
  32. package/lib/vendor/blamejs/lib/auth/jwt-external.js +2 -2
  33. package/lib/vendor/blamejs/lib/auth/oauth.js +9 -9
  34. package/lib/vendor/blamejs/lib/auth/oid4vci.js +7 -7
  35. package/lib/vendor/blamejs/lib/auth/oid4vp.js +1 -1
  36. package/lib/vendor/blamejs/lib/auth/openid-federation.js +5 -5
  37. package/lib/vendor/blamejs/lib/auth/passkey.js +6 -6
  38. package/lib/vendor/blamejs/lib/auth/saml.js +1 -1
  39. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +3 -6
  40. package/lib/vendor/blamejs/lib/backup/index.js +18 -18
  41. package/lib/vendor/blamejs/lib/cache.js +4 -4
  42. package/lib/vendor/blamejs/lib/calendar.js +5 -5
  43. package/lib/vendor/blamejs/lib/circuit-breaker.js +1 -1
  44. package/lib/vendor/blamejs/lib/cms-codec.js +2 -2
  45. package/lib/vendor/blamejs/lib/compliance.js +14 -14
  46. package/lib/vendor/blamejs/lib/crypto-field.js +58 -21
  47. package/lib/vendor/blamejs/lib/crypto.js +5 -6
  48. package/lib/vendor/blamejs/lib/db-query.js +131 -9
  49. package/lib/vendor/blamejs/lib/db.js +106 -22
  50. package/lib/vendor/blamejs/lib/external-db.js +64 -16
  51. package/lib/vendor/blamejs/lib/framework-schema.js +4 -4
  52. package/lib/vendor/blamejs/lib/guard-list-id.js +2 -2
  53. package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +1 -2
  54. package/lib/vendor/blamejs/lib/incident-report.js +150 -0
  55. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +1 -1
  56. package/lib/vendor/blamejs/lib/mail-deploy.js +3 -3
  57. package/lib/vendor/blamejs/lib/mail-server-managesieve.js +2 -2
  58. package/lib/vendor/blamejs/lib/mail-server-pop3.js +2 -2
  59. package/lib/vendor/blamejs/lib/mail-store.js +1 -1
  60. package/lib/vendor/blamejs/lib/metrics.js +8 -8
  61. package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +1 -1
  62. package/lib/vendor/blamejs/lib/middleware/dpop.js +5 -5
  63. package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +21 -22
  64. package/lib/vendor/blamejs/lib/middleware/protected-resource-metadata.js +2 -2
  65. package/lib/vendor/blamejs/lib/network-dns-resolver.js +2 -2
  66. package/lib/vendor/blamejs/lib/network-dns.js +1 -2
  67. package/lib/vendor/blamejs/lib/network-tls.js +0 -1
  68. package/lib/vendor/blamejs/lib/outbox.js +1 -1
  69. package/lib/vendor/blamejs/lib/pqc-agent.js +1 -1
  70. package/lib/vendor/blamejs/lib/retention.js +1 -1
  71. package/lib/vendor/blamejs/lib/retry.js +1 -1
  72. package/lib/vendor/blamejs/lib/safe-archive.js +2 -2
  73. package/lib/vendor/blamejs/lib/safe-ical.js +2 -2
  74. package/lib/vendor/blamejs/lib/safe-mime.js +1 -1
  75. package/lib/vendor/blamejs/lib/self-update-standalone-verifier.js +1 -1
  76. package/lib/vendor/blamejs/lib/self-update.js +2 -2
  77. package/lib/vendor/blamejs/lib/static.js +1 -1
  78. package/lib/vendor/blamejs/lib/subject.js +2 -2
  79. package/lib/vendor/blamejs/lib/vault/index.js +64 -1
  80. package/lib/vendor/blamejs/lib/vault/rotate.js +19 -0
  81. package/lib/vendor/blamejs/lib/vendor-data.js +1 -1
  82. package/lib/vendor/blamejs/package.json +1 -1
  83. package/lib/vendor/blamejs/release-notes/v0.14.7.json +77 -0
  84. package/lib/vendor/blamejs/scripts/release.js +28 -3
  85. package/lib/vendor/blamejs/test/layer-0-primitives/audit-tools-dual-control.test.js +115 -0
  86. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +163 -3
  87. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +102 -0
  88. package/lib/vendor/blamejs/test/layer-0-primitives/db-column-gate.test.js +150 -0
  89. package/lib/vendor/blamejs/test/layer-0-primitives/db-key-aad.test.js +109 -0
  90. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +53 -0
  91. package/lib/vendor/blamejs/test/layer-0-primitives/incident-report.test.js +65 -0
  92. package/lib/webhook-subscriptions.js +11 -24
  93. package/lib/webhooks.js +12 -33
  94. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.3.x
10
10
 
11
+ - v0.3.13 (2026-05-30) — **Updated runtime hardens the storage and audit layers; admin error pages no longer show raw database text.** The vendored blamejs runtime is updated to v0.14.7, which tightens the data and audit layers the shop is built on: database queries are checked against each table's declared columns (a reference to an undeclared column fails closed), the database encryption key is bound to its deployment so a copied key can't be unsealed elsewhere, raw SQL fragments refuse embedded string literals so values bind through placeholders, audit-chain purges can require two authorizers, and breach-notification deadlines raise a running clock. Separately, when an admin form action fails the console now shows a short generic message with the correct status instead of rendering a raw database or parser string into its error banner. No operator action is required. **Changed:** *Vendored runtime updated to v0.14.7 (storage + audit hardening)* — The underlying runtime now checks every database query against the table's declared columns and fails closed on an undeclared reference, binds the database encryption key to its data directory and key path so a relocated key won't unseal, refuses embedded string literals in raw SQL fragments, supports two-authorizer dual control on audit-chain purges, can compute sealed-column lookup hashes as a keyed MAC, and ships a running clock for breach-notification deadlines. The shop inherits these protections; no configuration change is needed. **Fixed:** *Console error banners show a safe message, not internal text* — Every admin HTML form handler now routes a thrown error through one shared classifier: a duplicate key becomes a 409 "That value is already in use.", a missing referenced record a generic not-found, malformed input a generic "Invalid input.", and an unexpected failure a generic message whose detail is recorded server-side rather than shown. Operator-facing validation messages are unchanged. This closes the path where a create form could render a raw database constraint string into its error banner, matching the API behavior the bearer path already had.
12
+
13
+ - v0.3.12 (2026-05-30) — **Currency and outbound-host validation consolidated behind one shared check.** Input validation that was duplicated across the codebase is now a single shared module. Currency codes are validated against the ISO 4217 catalog consistently wherever the admin binds money — the same check the gift-card flow gained last release — and the outbound webhook host guard (refusing loopback, private, link-local, reserved, and cloud-metadata destinations, including trailing-dot forms) is one shared function used by both the delivery and registration paths. The shared module composes the framework's Unicode codepoint catalog so free-text fields can reject control, bidi-override, and zero-width characters when needed. Behavior is equal-or-stricter than before; no operator action is required. **Changed:** *Currency validation is consistently ISO 4217* — Admin paths that bind a currency now validate it against the ISO 4217 catalog in one place, so a code that isn't a real currency is rejected consistently rather than only in some flows. · *Outbound-host SSRF guard is a single shared check* — The webhook delivery and registration paths now share one host guard — loopback, private (RFC 1918), link-local, reserved, and cloud-metadata addresses (by IP literal or known name, including trailing-dot forms) are refused identically on both, so the two can't drift apart.
14
+
11
15
  - v0.3.11 (2026-05-30) — **Webhook endpoints reject internal addresses; admin errors stay clean and typed.** Outbound webhook endpoints can no longer be pointed at loopback, private, link-local, or cloud-metadata addresses, closing a server-side request forgery path from operator-supplied URLs. Several admin endpoints that previously returned a 500 on bad input now return the correct 4xx — a duplicate slug or SKU is a 409 Conflict, a missing referenced record and malformed JSON are a 400 — and none of them echo internal database or parser text back in the response. Gift cards now reject a currency that isn't a real ISO 4217 code, and the returns refund action reports a malformed id the same way its sibling actions do. No configuration change is required; upgrading picks these up automatically. **Fixed:** *Admin errors return the right status and don't leak internals* — Creating a product, variant, or inventory row with a duplicate key now returns 409 Conflict instead of 500; attaching media to a missing product, and pasting malformed JSON into the shipping-zone editor, now return 400. None of these responses include raw database or JSON-parser text — the detail is a generic, operator-facing message and the underlying error is recorded server-side. The same applies to the receipt and packing-slip routes. · *Gift cards validate the currency* — Issuing a gift card with a currency that isn't a real ISO 4217 code is now rejected with a 400 instead of creating a card in a non-existent currency. · *Consistent error status for refunds* — The returns refund action reports a malformed return id with the same 400 its approve, receive, and reject siblings use; a well-formed id that doesn't exist is still a 404. **Security:** *Webhook endpoints can't target internal addresses* — A webhook endpoint URL that points at a loopback, private (RFC 1918), link-local, reserved, or cloud-metadata address — by IP literal or by a known name such as localhost, metadata.google.internal, or any *.internal host — is now refused when the subscription is created, on both the delivery and outbound-registration paths. A trailing-dot form of those hosts (for example localhost. or 169.254.169.254.) is normalized before the check so it can't slip past. Public https endpoints are unaffected.
12
16
 
13
17
  - v0.3.10 (2026-05-30) — **Order discounts are split across lines for accurate partial refunds.** When an order carries a cart-level discount, the admin console now records how that discount split across the order's lines — proportional to each line's value — so a later partial refund of one line knows that line's discounted share rather than the whole-order amount. A new read-only Discount splits screen shows the per-line breakdown for an order. This is back-office bookkeeping recorded after the order is placed; it has no effect on what the shopper is charged, and an order with no discount records nothing. **Added:** *Per-line discount allocation + admin view* — An order that carried an automatic cart discount now gets a recorded breakdown of how that discount split across its lines — proportional to each line's subtotal, summing exactly to the discount. A read-only Discount splits admin screen shows the breakdown for an order. The recording runs after the order is placed and never affects the charged amount; an order with no discount records nothing.
package/lib/admin.js CHANGED
@@ -33,6 +33,7 @@
33
33
  var pricing = require("./pricing");
34
34
  var collectionsModule = require("./collections");
35
35
  var quantityDiscountsModule = require("./quantity-discounts");
36
+ var textGuard = require("./text-guard");
36
37
  var { AsyncLocalStorage } = require("node:async_hooks"); // allow:non-shop-require — Node-core per-request context (no npm dep); the framework itself composes it in db-role-context / log. No b.* request-context primitive exists to wrap it.
37
38
 
38
39
  var b = require("./vendor/blamejs");
@@ -385,6 +386,59 @@ function _redirect(res, location) {
385
386
  if (res.end) res.end(); else res.send("");
386
387
  }
387
388
 
389
+ // Single classifier for a thrown admin error → an operator-safe outcome,
390
+ // shared by BOTH the bearer JSON path (_wrap) and every cookie/HTML form
391
+ // path so the two can never diverge on what a given error class is allowed
392
+ // to reveal. Returns `{ status, code, message }`:
393
+ //
394
+ // - TypeError → 400 bad-request, `e.message` verbatim. These are
395
+ // our own validation throws (`currency "ZZZ" is not
396
+ // a known ISO 4217 code`, `regions_json must be valid
397
+ // JSON`) — intended, operator-safe text the form must
398
+ // surface so the operator can correct the input.
399
+ // - SyntaxError → 400 bad-request, generic "Invalid input." A parser
400
+ // SyntaxError that reached here echoes the parse
401
+ // position ("...JSON at position 1"); never surface it.
402
+ // - UNIQUE / FOREIGN KEY constraint → 409 conflict, generic in-use /
403
+ // referenced-record text — never the table/column/SQL.
404
+ // - CHECK / NOT NULL constraint → 400 bad-request, generic
405
+ // missing-or-invalid text.
406
+ // - anything else → 500 internal-error, generic "Something went wrong
407
+ // — please try again." The raw message is recorded
408
+ // server-side via the framework audit (drop-silent,
409
+ // outcome:"failure") so an operator can correlate;
410
+ // it never reaches the client.
411
+ //
412
+ // `auditAction` (optional) names the audit action for the unknown-error
413
+ // record; defaults to "request" so a bare call still files under a sensible
414
+ // namespace.
415
+ function _safeNotice(e, auditAction) {
416
+ if (e instanceof TypeError) {
417
+ return { status: 400, code: "bad-request", message: (e && e.message) || "Invalid input." };
418
+ }
419
+ if (e instanceof SyntaxError) {
420
+ return { status: 400, code: "bad-request", message: "Invalid input." };
421
+ }
422
+ var msg = (e && e.message) || "";
423
+ if (/UNIQUE constraint failed/i.test(msg) || /FOREIGN KEY constraint failed/i.test(msg)) {
424
+ return /FOREIGN KEY/i.test(msg)
425
+ ? { status: 409, code: "conflict", message: "A referenced record does not exist." }
426
+ : { status: 409, code: "conflict", message: "That value is already in use." };
427
+ }
428
+ if (/(?:CHECK|NOT NULL) constraint failed/i.test(msg)) {
429
+ return { status: 400, code: "bad-request", message: "A required value is missing or invalid." };
430
+ }
431
+ // Genuine unknown error — record it server-side via the framework audit
432
+ // (drop-silent) so operators can correlate, then hand back a generic
433
+ // message + a 500. The raw message never reaches the client.
434
+ b.audit.safeEmit({
435
+ action: AUDIT_NAMESPACE + "." + (auditAction || "request") + ".error",
436
+ outcome: "failure",
437
+ metadata: { message: msg || String(e) },
438
+ });
439
+ return { status: 500, code: "internal-error", message: "Something went wrong — please try again." };
440
+ }
441
+
388
442
  function _wrap(handler, opts) {
389
443
  // Every admin handler routes through this wrapper: bearer-token
390
444
  // gate, error-to-problem-details translation, audit write on the
@@ -410,35 +464,13 @@ function _wrap(handler, opts) {
410
464
  }
411
465
  return result;
412
466
  } catch (e) {
413
- if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message);
414
- // Malformed JSON body the body parser raises a SyntaxError whose
415
- // message echoes the parser's position ("...JSON at position 1").
416
- // Surface a clean 400 with a generic detail rather than leaking the
417
- // parser internals (also the defense-in-depth net for any call site
418
- // that JSON.parses an operator-supplied field without its own guard).
419
- if (e instanceof SyntaxError) return _problem(res, 400, "bad-request", "Invalid JSON in request.");
420
- // Storage-engine constraint violations reach here as plain Errors
421
- // whose message names the table/column/SQL ("UNIQUE constraint
422
- // failed: products.slug"). Map the class to a clean 4xx with a
423
- // GENERIC operator-facing detail — never echo the table/column/SQL.
424
- var msg = (e && e.message) || "";
425
- if (/UNIQUE constraint failed/i.test(msg) || /FOREIGN KEY constraint failed/i.test(msg)) {
426
- return _problem(res, 409, "conflict",
427
- /FOREIGN KEY/i.test(msg) ? "A referenced record does not exist." : "That value is already in use.");
428
- }
429
- if (/(?:CHECK|NOT NULL) constraint failed/i.test(msg)) {
430
- return _problem(res, 400, "bad-request", "A required value is missing or invalid.");
431
- }
432
- // Genuine unknown error — record it server-side via the framework
433
- // audit (drop-silent) so operators can correlate, then return a
434
- // generic 500 with NO detail. The raw message never reaches the
435
- // client.
436
- b.audit.safeEmit({
437
- action: AUDIT_NAMESPACE + "." + (opts.audit || "request") + ".error",
438
- outcome: "failure",
439
- metadata: { message: msg || String(e) },
440
- });
441
- return _problem(res, 500, "internal-error");
467
+ // Single classification, shared with the cookie/HTML paths via
468
+ // _safeNotice. A 5xx carries NO error-derived detail (the unknown
469
+ // case is recorded server-side inside _safeNotice); 4xx surface the
470
+ // generic / validation message.
471
+ var n = _safeNotice(e, opts.audit);
472
+ if (n.status >= 500) return _problem(res, n.status, n.code);
473
+ return _problem(res, n.status, n.code, n.message);
442
474
  }
443
475
  };
444
476
  }
@@ -549,14 +581,12 @@ function mount(router, deps) {
549
581
  try {
550
582
  made = await catalog.products.create(req.body || {});
551
583
  } catch (e) {
552
- if (e instanceof TypeError || e.code === "CATALOG_DUPLICATE" || /slug|exists|duplicate/i.test(e.message || "")) {
553
- var page = await catalog.products.list({ limit: 100 });
554
- return _sendHtml(res, 400, renderAdminProducts({
555
- shop_name: deps.shop_name, nav_available: navAvailable, products: page.rows || [],
556
- notice: (e && e.message) || "Couldn't create that product.",
557
- }));
558
- }
559
- throw e;
584
+ var n = _safeNotice(e, "product.create");
585
+ var page = await catalog.products.list({ limit: 100 });
586
+ return _sendHtml(res, n.status, renderAdminProducts({
587
+ shop_name: deps.shop_name, nav_available: navAvailable, products: page.rows || [],
588
+ notice: n.message,
589
+ }));
560
590
  }
561
591
  b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".product.create", outcome: "success", metadata: { id: made.id } });
562
592
  _redirect(res, "/admin/products/" + encodeURIComponent(made.id) + "?created=1");
@@ -968,14 +998,12 @@ function mount(router, deps) {
968
998
  if (!body.sku) throw new TypeError("sku required");
969
999
  await catalog.inventory.create(body.sku, { stock_on_hand: parseInt(body.stock_on_hand, 10) || 0 });
970
1000
  } catch (e) {
971
- if (e instanceof TypeError || /exists|duplicate|UNIQUE/i.test(e.message || "")) {
972
- var page = await catalog.inventory.list({ limit: 500 });
973
- return _sendHtml(res, 400, renderAdminInventory({
974
- shop_name: deps.shop_name, nav_available: navAvailable, inventory: page.rows || [],
975
- notice: (e && e.message) || "Couldn't create that SKU.",
976
- }));
977
- }
978
- throw e;
1001
+ var n = _safeNotice(e, "inventory.create");
1002
+ var page = await catalog.inventory.list({ limit: 500 });
1003
+ return _sendHtml(res, n.status, renderAdminInventory({
1004
+ shop_name: deps.shop_name, nav_available: navAvailable, inventory: page.rows || [],
1005
+ notice: n.message,
1006
+ }));
979
1007
  }
980
1008
  b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".inventory.create", outcome: "success", metadata: { sku: body.sku } });
981
1009
  _redirect(res, "/admin/inventory?created=1");
@@ -1086,7 +1114,12 @@ function mount(router, deps) {
1086
1114
  alt_text: typeof body.alt_text === "string" ? body.alt_text : undefined,
1087
1115
  });
1088
1116
  } catch (e) {
1089
- if (!(e instanceof TypeError)) throw e;
1117
+ // Bad input (TypeError), a missing product (FOREIGN KEY constraint),
1118
+ // or any other failure → a notice via PRG. _safeNotice records an
1119
+ // unknown error server-side; the redirect itself carries no raw
1120
+ // message, so the storage-engine string can't ride the query into
1121
+ // the detail banner.
1122
+ _safeNotice(e, "media.attach");
1090
1123
  return _redirect(res, "/admin/products/" + enc + "?err=1");
1091
1124
  }
1092
1125
  b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".media.attach", outcome: "success", metadata: { id: id } });
@@ -1878,17 +1911,15 @@ function mount(router, deps) {
1878
1911
  var win = { to: Date.now(), from: Date.now() - b.constants.TIME.days(30) };
1879
1912
  try { status = _labelStatusParam(url); win = _labelWindow(url); }
1880
1913
  catch (e) {
1881
- if (!(e instanceof TypeError)) throw e;
1882
- notice = e.message.replace(/^admin:\s*/, "");
1914
+ notice = _safeNotice(e, "shipping_label.list").message.replace(/^admin:\s*/, "");
1883
1915
  }
1884
1916
  var payload;
1885
1917
  try { payload = await _labelListPayload(status, win); }
1886
1918
  catch (e2) {
1887
- if (!(e2 instanceof TypeError)) throw e2;
1888
1919
  // A bad range that slipped past the param parse (primitive
1889
1920
  // re-validates from < to) → fall back to the default window.
1890
1921
  win = { to: Date.now(), from: Date.now() - b.constants.TIME.days(30) };
1891
- notice = e2.message.replace(/^shipping-labels:\s*/, "");
1922
+ notice = _safeNotice(e2, "shipping_label.list").message.replace(/^shipping-labels:\s*/, "");
1892
1923
  payload = await _labelListPayload(status, win);
1893
1924
  }
1894
1925
  _sendHtml(res, 200, renderAdminShippingLabels({
@@ -1909,8 +1940,7 @@ function mount(router, deps) {
1909
1940
  var rows = [], notice = null;
1910
1941
  try { rows = await shippingLabels.pendingLabels({ limit: MAX_LABEL_LIST_LIMIT }); }
1911
1942
  catch (e) {
1912
- if (!(e instanceof TypeError)) throw e;
1913
- notice = e.message.replace(/^shipping-labels:\s*/, "");
1943
+ notice = _safeNotice(e, "shipping_label.pending").message.replace(/^shipping-labels:\s*/, "");
1914
1944
  }
1915
1945
  _sendHtml(res, 200, renderAdminShippingLabelsPending({
1916
1946
  shop_name: deps.shop_name, nav_available: navAvailable,
@@ -1936,16 +1966,14 @@ function mount(router, deps) {
1936
1966
  var win = { to: Date.now(), from: Date.now() - b.constants.TIME.days(30) };
1937
1967
  try { win = _labelWindow(url); carrier = _labelCarrierParam(url); }
1938
1968
  catch (e) {
1939
- if (!(e instanceof TypeError)) throw e;
1940
- notice = e.message.replace(/^admin:\s*/, "");
1969
+ notice = _safeNotice(e, "shipping_label.costs").message.replace(/^admin:\s*/, "");
1941
1970
  }
1942
1971
  var report;
1943
1972
  try { report = await _labelCostsReport(win, carrier); }
1944
1973
  catch (e2) {
1945
- if (!(e2 instanceof TypeError)) throw e2;
1946
1974
  win = { to: Date.now(), from: Date.now() - b.constants.TIME.days(30) };
1947
1975
  carrier = null;
1948
- notice = e2.message.replace(/^shipping-labels:\s*/, "");
1976
+ notice = _safeNotice(e2, "shipping_label.costs").message.replace(/^shipping-labels:\s*/, "");
1949
1977
  report = await _labelCostsReport(win, carrier);
1950
1978
  }
1951
1979
  _sendHtml(res, 200, renderAdminShippingLabelCosts({
@@ -3219,11 +3247,11 @@ function mount(router, deps) {
3219
3247
  try {
3220
3248
  ep = await webhooks.endpoints.create({ url: (typeof body.url === "string" ? body.url.trim() : body.url), events: events });
3221
3249
  } catch (e) {
3222
- if (!(e instanceof TypeError)) throw e;
3250
+ var n = _safeNotice(e, "webhook.create");
3223
3251
  var rows = await webhooks.endpoints.list();
3224
- return _sendHtml(res, 400, renderAdminWebhooks({
3252
+ return _sendHtml(res, n.status, renderAdminWebhooks({
3225
3253
  shop_name: deps.shop_name, nav_available: navAvailable, endpoints: rows,
3226
- known_events: KNOWN_WH_EVENTS, notice: e.message.replace(/^webhooks:\s*/, ""),
3254
+ known_events: KNOWN_WH_EVENTS, notice: n.message.replace(/^webhooks:\s*/, ""),
3227
3255
  }));
3228
3256
  }
3229
3257
  b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".webhook.create", outcome: "success", metadata: { id: ep.id } });
@@ -3458,10 +3486,6 @@ function mount(router, deps) {
3458
3486
  return n;
3459
3487
  }
3460
3488
 
3461
- function _cleanCreateMessage(e) {
3462
- return (e && e.message || "Couldn't create that collection.").replace(/^collections[.:]\s*/, "");
3463
- }
3464
-
3465
3489
  // Map the ?active= query to a collections.list filter: 1/true →
3466
3490
  // active-only, 0/false → archived-only, absent → all.
3467
3491
  function _collectionsFilter(activeS) {
@@ -3544,11 +3568,11 @@ function mount(router, deps) {
3544
3568
  });
3545
3569
  }
3546
3570
  } catch (e) {
3547
- if (!(e instanceof TypeError)) throw e;
3571
+ var n = _safeNotice(e, "collection.create");
3548
3572
  var rows = await _listForBrowser({});
3549
- return _sendHtml(res, 400, renderAdminCollections({
3573
+ return _sendHtml(res, n.status, renderAdminCollections({
3550
3574
  shop_name: deps.shop_name, nav_available: navAvailable, collections: rows,
3551
- notice: _cleanCreateMessage(e), form_type: type,
3575
+ notice: n.message.replace(/^collections[.:]\s*/, ""), form_type: type,
3552
3576
  }));
3553
3577
  }
3554
3578
  b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".collection.create", outcome: "success" });
@@ -3862,11 +3886,11 @@ function mount(router, deps) {
3862
3886
  try {
3863
3887
  await announcements.defineAnnouncement(_announcementFromForm(req.body || {}));
3864
3888
  } catch (e) {
3865
- if (!(e instanceof TypeError)) throw e;
3889
+ var n = _safeNotice(e, "announcement.define");
3866
3890
  var rows = await announcements.listAnnouncements({});
3867
- return _sendHtml(res, 400, renderAdminAnnouncements({
3891
+ return _sendHtml(res, n.status, renderAdminAnnouncements({
3868
3892
  shop_name: deps.shop_name, nav_available: navAvailable, announcements: rows,
3869
- notice: (e && e.message || "Couldn't save that announcement.").replace(/^announcementBar[.:]\s*/, ""),
3893
+ notice: n.message.replace(/^announcementBar[.:]\s*/, ""),
3870
3894
  }));
3871
3895
  }
3872
3896
  b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".announcement.define", outcome: "success" });
@@ -3941,11 +3965,11 @@ function mount(router, deps) {
3941
3965
  questions: _standardSurveyQuestions(body.kind),
3942
3966
  });
3943
3967
  } catch (e) {
3944
- if (!(e instanceof TypeError)) throw e;
3968
+ var n = _safeNotice(e, "survey.define");
3945
3969
  var rows = await surveys.listSurveys({});
3946
- return _sendHtml(res, 400, renderAdminSurveys({
3970
+ return _sendHtml(res, n.status, renderAdminSurveys({
3947
3971
  shop_name: deps.shop_name, nav_available: navAvailable, surveys: rows,
3948
- notice: (e.message || "Couldn't create that survey.").replace(/^customerSurveys[.:]\s*/, ""),
3972
+ notice: n.message.replace(/^customerSurveys[.:]\s*/, ""),
3949
3973
  }));
3950
3974
  }
3951
3975
  b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".survey.define", outcome: "success" });
@@ -4054,11 +4078,11 @@ function mount(router, deps) {
4054
4078
  try {
4055
4079
  await hours.defineSchedule(_scheduleFromForm(req.body || {}));
4056
4080
  } catch (e) {
4057
- if (!(e instanceof TypeError)) throw e;
4081
+ var n = _safeNotice(e, "hours.define");
4058
4082
  var rows = await hours.listSchedules();
4059
- return _sendHtml(res, 400, renderAdminHours({
4083
+ return _sendHtml(res, n.status, renderAdminHours({
4060
4084
  shop_name: deps.shop_name, nav_available: navAvailable, schedules: rows,
4061
- notice: (e.message || "Couldn't save that schedule.").replace(/^businessHours[.:]\s*/, ""),
4085
+ notice: n.message.replace(/^businessHours[.:]\s*/, ""),
4062
4086
  }));
4063
4087
  }
4064
4088
  b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".hours.define", outcome: "success" });
@@ -4239,11 +4263,10 @@ function mount(router, deps) {
4239
4263
  var win, notice = null;
4240
4264
  try { win = _reportWindow(url); }
4241
4265
  catch (e) {
4242
- if (!(e instanceof TypeError)) throw e;
4243
4266
  // Bad range → re-render with the default window + a correction
4244
4267
  // notice (config/entry tier: the operator fixes the typo).
4245
4268
  win = { to: Date.now(), from: Date.now() - b.constants.TIME.days(30) };
4246
- notice = e.message.replace(/^admin:\s*/, "");
4269
+ notice = _safeNotice(e, "report.view").message.replace(/^admin:\s*/, "");
4247
4270
  }
4248
4271
  // CSV download from the browser surface too (a link, not a fetch).
4249
4272
  if (url && url.searchParams.get("format") === "csv") {
@@ -4264,9 +4287,8 @@ function mount(router, deps) {
4264
4287
  var report;
4265
4288
  try { report = await _buildReport(win); }
4266
4289
  catch (e3) {
4267
- if (!(e3 instanceof TypeError)) throw e3;
4268
4290
  win = { to: Date.now(), from: Date.now() - b.constants.TIME.days(30) };
4269
- notice = e3.message.replace(/^salesReports:\s*/, "");
4291
+ notice = _safeNotice(e3, "report.view").message.replace(/^salesReports:\s*/, "");
4270
4292
  report = await _buildReport(win);
4271
4293
  }
4272
4294
  _sendHtml(res, 200, renderAdminReports({
@@ -4466,11 +4488,11 @@ function mount(router, deps) {
4466
4488
  try {
4467
4489
  await subscriptions.plans.create(input);
4468
4490
  } catch (e) {
4469
- if (!(e instanceof TypeError)) throw e;
4491
+ var n = _safeNotice(e, "subscription_plan.create");
4470
4492
  var rows = await subscriptions.plans.list({});
4471
- return _sendHtml(res, 400, renderAdminSubscriptionPlans({
4493
+ return _sendHtml(res, n.status, renderAdminSubscriptionPlans({
4472
4494
  shop_name: deps.shop_name, nav_available: navAvailable, plans: rows,
4473
- notice: e.message.replace(/^subscriptions[.:]\s*/, ""),
4495
+ notice: n.message.replace(/^subscriptions[.:]\s*/, ""),
4474
4496
  }));
4475
4497
  }
4476
4498
  b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".subscription_plan.create", outcome: "success" });
@@ -4628,11 +4650,11 @@ function mount(router, deps) {
4628
4650
  try {
4629
4651
  issued = await _issueGiftCard(req.body || {});
4630
4652
  } catch (e) {
4631
- if (!(e instanceof TypeError)) throw e;
4653
+ var n = _safeNotice(e, "gift_card.issue");
4632
4654
  var rows = await giftcards.list({});
4633
- return _sendHtml(res, 400, renderAdminGiftCards({
4655
+ return _sendHtml(res, n.status, renderAdminGiftCards({
4634
4656
  shop_name: deps.shop_name, nav_available: navAvailable, cards: rows,
4635
- notice: e.message.replace(/^giftcards?[.:]\s*/i, ""),
4657
+ notice: n.message.replace(/^giftcards?[.:]\s*/i, ""),
4636
4658
  }));
4637
4659
  }
4638
4660
  b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".gift_card.issue", outcome: "success", metadata: { id: issued.id } });
@@ -4769,10 +4791,7 @@ function mount(router, deps) {
4769
4791
  // framework's ISO 4217 catalog (the same b.money.CURRENCIES surface the
4770
4792
  // currency-rounding + display primitives compose) and refuse unknown
4771
4793
  // codes with a clean 400.
4772
- if (typeof input.currency === "string" && /^[A-Z]{3}$/.test(input.currency) &&
4773
- !Object.prototype.hasOwnProperty.call(b.money.CURRENCIES, input.currency)) {
4774
- throw new TypeError("giftcards: currency " + JSON.stringify(input.currency) + " is not a known ISO 4217 code");
4775
- }
4794
+ textGuard.currencyCode(input.currency, "giftcards: currency");
4776
4795
  if (body.amount_minor != null && body.amount_minor !== "") {
4777
4796
  input.amount_minor = _strictMinorInt(body.amount_minor, "giftcards", "amount_minor (minor units)");
4778
4797
  }
@@ -4823,12 +4842,18 @@ function mount(router, deps) {
4823
4842
  try {
4824
4843
  await taxRates.defineRate(_taxRateInput(body));
4825
4844
  } catch (e) {
4826
- if (!(e instanceof TypeError) && e.code !== "TAX_RATE_OVERLAP") throw e;
4845
+ // The primitive's overlap code carries an operator-safe message;
4846
+ // everything else routes through the shared classifier so a raw
4847
+ // constraint / parser / unknown error can't reach the banner.
4848
+ var overlap = e && e.code === "TAX_RATE_OVERLAP";
4849
+ var n = overlap ? { status: 400, message: (e.message || "").replace(/^taxRates[.:]\s*/, "") }
4850
+ : _safeNotice(e, "tax_rate.create");
4851
+ var noticeMsg = overlap ? n.message : n.message.replace(/^taxRates[.:]\s*/, "");
4827
4852
  var rows = await _taxRatesForBrowser(jurisdiction);
4828
- return _sendHtml(res, 400, renderAdminTaxRates({
4853
+ return _sendHtml(res, n.status, renderAdminTaxRates({
4829
4854
  shop_name: deps.shop_name, nav_available: navAvailable, rates: rows,
4830
4855
  jurisdiction: jurisdiction, sources: taxRates.SOURCES,
4831
- notice: (e && e.message || "").replace(/^taxRates[.:]\s*/, ""),
4856
+ notice: noticeMsg,
4832
4857
  }));
4833
4858
  }
4834
4859
  b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".tax_rate.create", outcome: "success" });
@@ -5016,14 +5041,21 @@ function mount(router, deps) {
5016
5041
  var filing;
5017
5042
  try { filing = await salesTaxFilings.defineFilingPeriod(_filingPeriodInput(req.body || {})); }
5018
5043
  catch (e) {
5019
- if (!(e instanceof TypeError) && e.code !== "SALES_TAX_FILING_DUPLICATE") throw e;
5044
+ // The primitive's own duplicate-period code carries an operator-safe
5045
+ // message (a named UNIQUE index, no raw SQL); everything else routes
5046
+ // through the shared classifier so a raw constraint / parser / unknown
5047
+ // error can't reach the banner.
5048
+ var dup = e && e.code === "SALES_TAX_FILING_DUPLICATE";
5049
+ var n = dup ? { status: 400, message: (e.message || "").replace(/^salesTaxFilings[.:]\s*/, "") }
5050
+ : _safeNotice(e, "tax_filing.create");
5051
+ var noticeMsg = dup ? n.message : n.message.replace(/^salesTaxFilings[.:]\s*/, "");
5020
5052
  var rows = await _filingsForBrowser({});
5021
5053
  var upcoming = await _upcomingForBrowser();
5022
- return _sendHtml(res, 400, renderAdminTaxFilings({
5054
+ return _sendHtml(res, n.status, renderAdminTaxFilings({
5023
5055
  shop_name: deps.shop_name, nav_available: navAvailable,
5024
5056
  filings: rows, upcoming: upcoming,
5025
5057
  kinds: salesTaxFilings.KINDS, statuses: salesTaxFilings.STATUSES,
5026
- notice: (e && e.message || "").replace(/^salesTaxFilings[.:]\s*/, ""),
5058
+ notice: noticeMsg,
5027
5059
  }));
5028
5060
  }
5029
5061
  b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".tax_filing.create", outcome: "success", metadata: { id: filing.id } });
@@ -5066,7 +5098,7 @@ function mount(router, deps) {
5066
5098
  notice = "Enter both a from and a to date (epoch-ms) for the report window.";
5067
5099
  } else {
5068
5100
  try { report = await salesTaxFilings.auditReportForJurisdiction({ jurisdiction: jurisdiction, from: from, to: to }); }
5069
- catch (e) { if (!(e instanceof TypeError)) throw e; notice = (e.message || "").replace(/^salesTaxFilings[.:]\s*/, ""); }
5101
+ catch (e) { notice = _safeNotice(e, "tax_filing.report").message.replace(/^salesTaxFilings[.:]\s*/, ""); }
5070
5102
  }
5071
5103
  _sendHtml(res, 200, renderAdminTaxFilingReport({
5072
5104
  shop_name: deps.shop_name, nav_available: navAvailable,
@@ -5134,10 +5166,13 @@ function mount(router, deps) {
5134
5166
  try {
5135
5167
  await run(req.params.id, req.body || {});
5136
5168
  } catch (e) {
5137
- if (!(e instanceof TypeError) &&
5138
- e.code !== "SALES_TAX_FILING_NOT_FOUND" &&
5139
- e.code !== "SALES_TAX_FILING_BAD_TRANSITION") throw e;
5140
- var msg = (e && e.message || "").replace(/^salesTaxFilings[.:]\s*/, "");
5169
+ // The primitive's not-found / bad-transition codes carry
5170
+ // operator-safe messages; everything else routes through the
5171
+ // shared classifier so a raw constraint / parser / unknown error
5172
+ // can't ride the err_msg query param into the detail banner.
5173
+ var safe = e && (e.code === "SALES_TAX_FILING_NOT_FOUND" || e.code === "SALES_TAX_FILING_BAD_TRANSITION");
5174
+ var msg = (safe ? (e.message || "") : _safeNotice(e, "tax_filing." + audit).message)
5175
+ .replace(/^salesTaxFilings[.:]\s*/, "");
5141
5176
  return _redirect(res, "/admin/tax-filings/" + enc + "?err=1&err_msg=" + encodeURIComponent(msg));
5142
5177
  }
5143
5178
  b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".tax_filing." + audit, outcome: "success", metadata: { id: req.params.id } });
@@ -5234,11 +5269,17 @@ function mount(router, deps) {
5234
5269
  try {
5235
5270
  await shippingZones.defineZone(_shippingZoneInput(req.body || {}));
5236
5271
  } catch (e) {
5237
- if (!(e instanceof TypeError) && e.code !== "SHIPPING_ZONE_EXISTS") throw e;
5272
+ // The primitive's already-exists code carries an operator-safe
5273
+ // message; everything else routes through the shared classifier so
5274
+ // a raw constraint / parser / unknown error can't reach the banner.
5275
+ var dup = e && e.code === "SHIPPING_ZONE_EXISTS";
5276
+ var n = dup ? { status: 400, message: (e.message || "").replace(/^shippingZones[.:]\s*/, "") }
5277
+ : _safeNotice(e, "shipping_zone.create");
5278
+ var noticeMsg = dup ? n.message : n.message.replace(/^shippingZones[.:]\s*/, "");
5238
5279
  var rows = await shippingZones.listZones({});
5239
- return _sendHtml(res, 400, renderAdminShipping({
5280
+ return _sendHtml(res, n.status, renderAdminShipping({
5240
5281
  shop_name: deps.shop_name, nav_available: navAvailable, zones: rows,
5241
- notice: (e && e.message || "").replace(/^shippingZones[.:]\s*/, ""),
5282
+ notice: noticeMsg,
5242
5283
  }));
5243
5284
  }
5244
5285
  b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".shipping_zone.create", outcome: "success" });
@@ -5432,9 +5473,9 @@ function mount(router, deps) {
5432
5473
  try {
5433
5474
  await autoDiscount.defineRule(_discountInput(req.body || {}));
5434
5475
  } catch (e) {
5435
- if (!(e instanceof TypeError)) throw e;
5436
- return _sendHtml(res, 400, await _renderDiscounts({
5437
- notice: (e && e.message || "").replace(/^autoDiscount[.:]\s*/, ""),
5476
+ var n = _safeNotice(e, "auto_discount.create");
5477
+ return _sendHtml(res, n.status, await _renderDiscounts({
5478
+ notice: n.message.replace(/^autoDiscount[.:]\s*/, ""),
5438
5479
  }));
5439
5480
  }
5440
5481
  b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".auto_discount.create", outcome: "success" });
@@ -5525,9 +5566,9 @@ function mount(router, deps) {
5525
5566
  try {
5526
5567
  await couponStacking.definePolicy(_policyInput(req.body || {}));
5527
5568
  } catch (e) {
5528
- if (!(e instanceof TypeError)) throw e;
5529
- return _sendHtml(res, 400, await _renderDiscounts({
5530
- notice: (e && e.message || "").replace(/^couponStacking[.:]\s*/, ""),
5569
+ var n = _safeNotice(e, "coupon_policy.create");
5570
+ return _sendHtml(res, n.status, await _renderDiscounts({
5571
+ notice: n.message.replace(/^couponStacking[.:]\s*/, ""),
5531
5572
  }));
5532
5573
  }
5533
5574
  b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".coupon_policy.create", outcome: "success" });
@@ -5641,10 +5682,6 @@ function mount(router, deps) {
5641
5682
  return input;
5642
5683
  }
5643
5684
 
5644
- function _qdCleanMessage(e) {
5645
- return (e && e.message || "Couldn't save that tier set.").replace(/^quantityDiscounts[.:]\s*/, "");
5646
- }
5647
-
5648
5685
  async function _renderQdList(flags) {
5649
5686
  flags = flags || {};
5650
5687
  var rows = await quantityDiscounts.list(_qdFilter(flags.archived_filter));
@@ -5684,8 +5721,10 @@ function mount(router, deps) {
5684
5721
  try {
5685
5722
  await quantityDiscounts.defineTier(_qdDefineInput(req.body || {}));
5686
5723
  } catch (e) {
5687
- if (!(e instanceof TypeError)) throw e;
5688
- return _sendHtml(res, 400, await _renderQdList({ notice: _qdCleanMessage(e) }));
5724
+ var n = _safeNotice(e, "quantity_discount.create");
5725
+ return _sendHtml(res, n.status, await _renderQdList({
5726
+ notice: n.message.replace(/^quantityDiscounts[.:]\s*/, ""),
5727
+ }));
5689
5728
  }
5690
5729
  b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".quantity_discount.create", outcome: "success" });
5691
5730
  _redirect(res, "/admin/quantity-discounts?created=1");
@@ -6029,9 +6068,10 @@ function mount(router, deps) {
6029
6068
  if (values.support_url) await config.put("shop.support_url", values.support_url);
6030
6069
  await config.put("setup.completed", true);
6031
6070
  } catch (e) {
6032
- return _sendHtml(res, 500, renderAdminSetup({
6071
+ var n = _safeNotice(e, "setup.save");
6072
+ return _sendHtml(res, n.status, renderAdminSetup({
6033
6073
  shop_name: deps.shop_name, values: values, nav_available: navAvailable,
6034
- notice: "Couldn't save — " + ((e && e.message) || "please try again."),
6074
+ notice: n.message,
6035
6075
  }));
6036
6076
  }
6037
6077
  b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".setup.save", outcome: "success", metadata: {} });
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.3.11",
2
+ "version": "0.3.13",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-1SIn6oAf1DjECbRfKENZasdKHJiywGdXR58wn0hsFGcdVHzUmvfgMkEz5ANIAZJ3",
@@ -81,6 +81,7 @@
81
81
  */
82
82
 
83
83
  var b = require("./vendor/blamejs");
84
+ var textGuard = require("./text-guard");
84
85
 
85
86
  // ---- constants ----------------------------------------------------------
86
87
 
@@ -97,20 +98,7 @@ var MAX_LIST_LIMIT = 500;
97
98
  // typo at config-time rather than at the first checkout that touches
98
99
  // the bad code.
99
100
  function _currency(code) {
100
- if (typeof code !== "string" || !/^[A-Z]{3}$/.test(code)) {
101
- throw new TypeError(
102
- "currencyRounding: currency must be a 3-letter uppercase ISO 4217 code, got " +
103
- JSON.stringify(code)
104
- );
105
- }
106
- var catalog = b.money.CURRENCIES;
107
- if (!Object.prototype.hasOwnProperty.call(catalog, code)) {
108
- throw new TypeError(
109
- "currencyRounding: currency " + JSON.stringify(code) +
110
- " is not in the ISO 4217 catalog"
111
- );
112
- }
113
- return code;
101
+ return textGuard.currencyCode(code, "currencyRounding: currency");
114
102
  }
115
103
 
116
104
  function _incrementMinor(n) {
package/lib/index.js CHANGED
@@ -252,4 +252,5 @@ Object.assign(module.exports, {
252
252
  winbackCampaigns: require("./winback-campaigns"),
253
253
  wishlistDigest: require("./wishlist-digest"),
254
254
  operatorHelpCenter: require("./operator-help-center"),
255
+ textGuard: require("./text-guard"),
255
256
  });