@blamejs/blamejs-shop 0.3.12 → 0.3.14
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 +155 -113
- package/lib/asset-manifest.json +1 -1
- package/lib/security-middleware.js +28 -0
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/README.md +3 -2
- package/lib/vendor/blamejs/SECURITY.md +3 -0
- package/lib/vendor/blamejs/api-snapshot.json +14 -2
- package/lib/vendor/blamejs/lib/agent-event-bus.js +4 -4
- package/lib/vendor/blamejs/lib/agent-idempotency.js +6 -6
- package/lib/vendor/blamejs/lib/agent-orchestrator.js +9 -9
- package/lib/vendor/blamejs/lib/agent-posture-chain.js +10 -10
- package/lib/vendor/blamejs/lib/agent-saga.js +6 -7
- package/lib/vendor/blamejs/lib/agent-snapshot.js +8 -8
- package/lib/vendor/blamejs/lib/agent-stream.js +3 -3
- package/lib/vendor/blamejs/lib/agent-tenant.js +4 -4
- package/lib/vendor/blamejs/lib/agent-trace.js +5 -5
- package/lib/vendor/blamejs/lib/ai-disclosure.js +3 -3
- package/lib/vendor/blamejs/lib/app.js +2 -2
- package/lib/vendor/blamejs/lib/archive-read.js +1 -1
- package/lib/vendor/blamejs/lib/archive-tar-read.js +1 -1
- package/lib/vendor/blamejs/lib/archive-wrap.js +5 -5
- package/lib/vendor/blamejs/lib/audit-tools.js +65 -5
- package/lib/vendor/blamejs/lib/audit.js +2 -2
- package/lib/vendor/blamejs/lib/auth/ciba.js +1 -1
- package/lib/vendor/blamejs/lib/auth/dpop.js +1 -1
- package/lib/vendor/blamejs/lib/auth/fal.js +1 -1
- package/lib/vendor/blamejs/lib/auth/fido-mds3.js +2 -3
- package/lib/vendor/blamejs/lib/auth/jwt-external.js +2 -2
- package/lib/vendor/blamejs/lib/auth/oauth.js +9 -9
- package/lib/vendor/blamejs/lib/auth/oid4vci.js +7 -7
- package/lib/vendor/blamejs/lib/auth/oid4vp.js +1 -1
- package/lib/vendor/blamejs/lib/auth/openid-federation.js +5 -5
- package/lib/vendor/blamejs/lib/auth/passkey.js +6 -6
- package/lib/vendor/blamejs/lib/auth/saml.js +1 -1
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +3 -6
- package/lib/vendor/blamejs/lib/backup/index.js +18 -18
- package/lib/vendor/blamejs/lib/cache.js +4 -4
- package/lib/vendor/blamejs/lib/calendar.js +5 -5
- package/lib/vendor/blamejs/lib/circuit-breaker.js +1 -1
- package/lib/vendor/blamejs/lib/cms-codec.js +2 -2
- package/lib/vendor/blamejs/lib/compliance.js +14 -14
- package/lib/vendor/blamejs/lib/crypto-field.js +58 -21
- package/lib/vendor/blamejs/lib/crypto.js +5 -6
- package/lib/vendor/blamejs/lib/db-query.js +131 -9
- package/lib/vendor/blamejs/lib/db.js +106 -22
- package/lib/vendor/blamejs/lib/external-db.js +64 -16
- package/lib/vendor/blamejs/lib/framework-schema.js +4 -4
- package/lib/vendor/blamejs/lib/guard-list-id.js +2 -2
- package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +1 -2
- package/lib/vendor/blamejs/lib/incident-report.js +150 -0
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +1 -1
- package/lib/vendor/blamejs/lib/mail-deploy.js +3 -3
- package/lib/vendor/blamejs/lib/mail-server-managesieve.js +2 -2
- package/lib/vendor/blamejs/lib/mail-server-pop3.js +2 -2
- package/lib/vendor/blamejs/lib/mail-store.js +1 -1
- package/lib/vendor/blamejs/lib/metrics.js +8 -8
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +1 -1
- package/lib/vendor/blamejs/lib/middleware/dpop.js +5 -5
- package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +21 -22
- package/lib/vendor/blamejs/lib/middleware/protected-resource-metadata.js +2 -2
- package/lib/vendor/blamejs/lib/network-dns-resolver.js +2 -2
- package/lib/vendor/blamejs/lib/network-dns.js +1 -2
- package/lib/vendor/blamejs/lib/network-tls.js +0 -1
- package/lib/vendor/blamejs/lib/outbox.js +1 -1
- package/lib/vendor/blamejs/lib/pqc-agent.js +1 -1
- package/lib/vendor/blamejs/lib/retention.js +1 -1
- package/lib/vendor/blamejs/lib/retry.js +1 -1
- package/lib/vendor/blamejs/lib/safe-archive.js +2 -2
- package/lib/vendor/blamejs/lib/safe-ical.js +2 -2
- package/lib/vendor/blamejs/lib/safe-mime.js +1 -1
- package/lib/vendor/blamejs/lib/self-update-standalone-verifier.js +1 -1
- package/lib/vendor/blamejs/lib/self-update.js +2 -2
- package/lib/vendor/blamejs/lib/static.js +1 -1
- package/lib/vendor/blamejs/lib/subject.js +2 -2
- package/lib/vendor/blamejs/lib/vault/index.js +64 -1
- package/lib/vendor/blamejs/lib/vault/rotate.js +19 -0
- package/lib/vendor/blamejs/lib/vendor-data.js +1 -1
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.7.json +77 -0
- package/lib/vendor/blamejs/scripts/release.js +28 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-tools-dual-control.test.js +115 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +163 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +102 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-column-gate.test.js +150 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-key-aad.test.js +109 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +53 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/incident-report.test.js +65 -0
- 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.14 (2026-05-30) — **Drop the inert Document-Policy header that only produced browser warnings.** Container-rendered pages were sending a Document-Policy response header whose feature tokens (document-write, unsized-media, oversized-images) are no longer recognized by current browsers — so it enforced nothing while making the browser log a warning for each token on every page. The header is now omitted; every other security header (Content-Security-Policy, Permissions-Policy, HSTS, X-Frame-Options, the COOP/CORP set, and the rest) is unchanged. The edge and container paths now agree — neither sends Document-Policy — closing a header-parity gap. **Fixed:** *No more Document-Policy console warnings* — The Document-Policy header is no longer emitted on container-rendered pages. Its tokens were rejected by current browsers (the policy applied nothing and logged a warning per token on every page), so the header is dropped rather than shipped inert; the full set of other security headers is retained unchanged. Edge and container responses are now consistent.
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
11
15
|
- 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.
|
|
12
16
|
|
|
13
17
|
- 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.
|
package/lib/admin.js
CHANGED
|
@@ -386,6 +386,59 @@ function _redirect(res, location) {
|
|
|
386
386
|
if (res.end) res.end(); else res.send("");
|
|
387
387
|
}
|
|
388
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
|
+
|
|
389
442
|
function _wrap(handler, opts) {
|
|
390
443
|
// Every admin handler routes through this wrapper: bearer-token
|
|
391
444
|
// gate, error-to-problem-details translation, audit write on the
|
|
@@ -411,35 +464,13 @@ function _wrap(handler, opts) {
|
|
|
411
464
|
}
|
|
412
465
|
return result;
|
|
413
466
|
} catch (e) {
|
|
414
|
-
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
// Storage-engine constraint violations reach here as plain Errors
|
|
422
|
-
// whose message names the table/column/SQL ("UNIQUE constraint
|
|
423
|
-
// failed: products.slug"). Map the class to a clean 4xx with a
|
|
424
|
-
// GENERIC operator-facing detail — never echo the table/column/SQL.
|
|
425
|
-
var msg = (e && e.message) || "";
|
|
426
|
-
if (/UNIQUE constraint failed/i.test(msg) || /FOREIGN KEY constraint failed/i.test(msg)) {
|
|
427
|
-
return _problem(res, 409, "conflict",
|
|
428
|
-
/FOREIGN KEY/i.test(msg) ? "A referenced record does not exist." : "That value is already in use.");
|
|
429
|
-
}
|
|
430
|
-
if (/(?:CHECK|NOT NULL) constraint failed/i.test(msg)) {
|
|
431
|
-
return _problem(res, 400, "bad-request", "A required value is missing or invalid.");
|
|
432
|
-
}
|
|
433
|
-
// Genuine unknown error — record it server-side via the framework
|
|
434
|
-
// audit (drop-silent) so operators can correlate, then return a
|
|
435
|
-
// generic 500 with NO detail. The raw message never reaches the
|
|
436
|
-
// client.
|
|
437
|
-
b.audit.safeEmit({
|
|
438
|
-
action: AUDIT_NAMESPACE + "." + (opts.audit || "request") + ".error",
|
|
439
|
-
outcome: "failure",
|
|
440
|
-
metadata: { message: msg || String(e) },
|
|
441
|
-
});
|
|
442
|
-
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);
|
|
443
474
|
}
|
|
444
475
|
};
|
|
445
476
|
}
|
|
@@ -550,14 +581,12 @@ function mount(router, deps) {
|
|
|
550
581
|
try {
|
|
551
582
|
made = await catalog.products.create(req.body || {});
|
|
552
583
|
} catch (e) {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
560
|
-
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
|
+
}));
|
|
561
590
|
}
|
|
562
591
|
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".product.create", outcome: "success", metadata: { id: made.id } });
|
|
563
592
|
_redirect(res, "/admin/products/" + encodeURIComponent(made.id) + "?created=1");
|
|
@@ -969,14 +998,12 @@ function mount(router, deps) {
|
|
|
969
998
|
if (!body.sku) throw new TypeError("sku required");
|
|
970
999
|
await catalog.inventory.create(body.sku, { stock_on_hand: parseInt(body.stock_on_hand, 10) || 0 });
|
|
971
1000
|
} catch (e) {
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
}
|
|
979
|
-
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
|
+
}));
|
|
980
1007
|
}
|
|
981
1008
|
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".inventory.create", outcome: "success", metadata: { sku: body.sku } });
|
|
982
1009
|
_redirect(res, "/admin/inventory?created=1");
|
|
@@ -1087,7 +1114,12 @@ function mount(router, deps) {
|
|
|
1087
1114
|
alt_text: typeof body.alt_text === "string" ? body.alt_text : undefined,
|
|
1088
1115
|
});
|
|
1089
1116
|
} catch (e) {
|
|
1090
|
-
|
|
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");
|
|
1091
1123
|
return _redirect(res, "/admin/products/" + enc + "?err=1");
|
|
1092
1124
|
}
|
|
1093
1125
|
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".media.attach", outcome: "success", metadata: { id: id } });
|
|
@@ -1879,17 +1911,15 @@ function mount(router, deps) {
|
|
|
1879
1911
|
var win = { to: Date.now(), from: Date.now() - b.constants.TIME.days(30) };
|
|
1880
1912
|
try { status = _labelStatusParam(url); win = _labelWindow(url); }
|
|
1881
1913
|
catch (e) {
|
|
1882
|
-
|
|
1883
|
-
notice = e.message.replace(/^admin:\s*/, "");
|
|
1914
|
+
notice = _safeNotice(e, "shipping_label.list").message.replace(/^admin:\s*/, "");
|
|
1884
1915
|
}
|
|
1885
1916
|
var payload;
|
|
1886
1917
|
try { payload = await _labelListPayload(status, win); }
|
|
1887
1918
|
catch (e2) {
|
|
1888
|
-
if (!(e2 instanceof TypeError)) throw e2;
|
|
1889
1919
|
// A bad range that slipped past the param parse (primitive
|
|
1890
1920
|
// re-validates from < to) → fall back to the default window.
|
|
1891
1921
|
win = { to: Date.now(), from: Date.now() - b.constants.TIME.days(30) };
|
|
1892
|
-
notice = e2.message.replace(/^shipping-labels:\s*/, "");
|
|
1922
|
+
notice = _safeNotice(e2, "shipping_label.list").message.replace(/^shipping-labels:\s*/, "");
|
|
1893
1923
|
payload = await _labelListPayload(status, win);
|
|
1894
1924
|
}
|
|
1895
1925
|
_sendHtml(res, 200, renderAdminShippingLabels({
|
|
@@ -1910,8 +1940,7 @@ function mount(router, deps) {
|
|
|
1910
1940
|
var rows = [], notice = null;
|
|
1911
1941
|
try { rows = await shippingLabels.pendingLabels({ limit: MAX_LABEL_LIST_LIMIT }); }
|
|
1912
1942
|
catch (e) {
|
|
1913
|
-
|
|
1914
|
-
notice = e.message.replace(/^shipping-labels:\s*/, "");
|
|
1943
|
+
notice = _safeNotice(e, "shipping_label.pending").message.replace(/^shipping-labels:\s*/, "");
|
|
1915
1944
|
}
|
|
1916
1945
|
_sendHtml(res, 200, renderAdminShippingLabelsPending({
|
|
1917
1946
|
shop_name: deps.shop_name, nav_available: navAvailable,
|
|
@@ -1937,16 +1966,14 @@ function mount(router, deps) {
|
|
|
1937
1966
|
var win = { to: Date.now(), from: Date.now() - b.constants.TIME.days(30) };
|
|
1938
1967
|
try { win = _labelWindow(url); carrier = _labelCarrierParam(url); }
|
|
1939
1968
|
catch (e) {
|
|
1940
|
-
|
|
1941
|
-
notice = e.message.replace(/^admin:\s*/, "");
|
|
1969
|
+
notice = _safeNotice(e, "shipping_label.costs").message.replace(/^admin:\s*/, "");
|
|
1942
1970
|
}
|
|
1943
1971
|
var report;
|
|
1944
1972
|
try { report = await _labelCostsReport(win, carrier); }
|
|
1945
1973
|
catch (e2) {
|
|
1946
|
-
if (!(e2 instanceof TypeError)) throw e2;
|
|
1947
1974
|
win = { to: Date.now(), from: Date.now() - b.constants.TIME.days(30) };
|
|
1948
1975
|
carrier = null;
|
|
1949
|
-
notice = e2.message.replace(/^shipping-labels:\s*/, "");
|
|
1976
|
+
notice = _safeNotice(e2, "shipping_label.costs").message.replace(/^shipping-labels:\s*/, "");
|
|
1950
1977
|
report = await _labelCostsReport(win, carrier);
|
|
1951
1978
|
}
|
|
1952
1979
|
_sendHtml(res, 200, renderAdminShippingLabelCosts({
|
|
@@ -3220,11 +3247,11 @@ function mount(router, deps) {
|
|
|
3220
3247
|
try {
|
|
3221
3248
|
ep = await webhooks.endpoints.create({ url: (typeof body.url === "string" ? body.url.trim() : body.url), events: events });
|
|
3222
3249
|
} catch (e) {
|
|
3223
|
-
|
|
3250
|
+
var n = _safeNotice(e, "webhook.create");
|
|
3224
3251
|
var rows = await webhooks.endpoints.list();
|
|
3225
|
-
return _sendHtml(res,
|
|
3252
|
+
return _sendHtml(res, n.status, renderAdminWebhooks({
|
|
3226
3253
|
shop_name: deps.shop_name, nav_available: navAvailable, endpoints: rows,
|
|
3227
|
-
known_events: KNOWN_WH_EVENTS, notice:
|
|
3254
|
+
known_events: KNOWN_WH_EVENTS, notice: n.message.replace(/^webhooks:\s*/, ""),
|
|
3228
3255
|
}));
|
|
3229
3256
|
}
|
|
3230
3257
|
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".webhook.create", outcome: "success", metadata: { id: ep.id } });
|
|
@@ -3459,10 +3486,6 @@ function mount(router, deps) {
|
|
|
3459
3486
|
return n;
|
|
3460
3487
|
}
|
|
3461
3488
|
|
|
3462
|
-
function _cleanCreateMessage(e) {
|
|
3463
|
-
return (e && e.message || "Couldn't create that collection.").replace(/^collections[.:]\s*/, "");
|
|
3464
|
-
}
|
|
3465
|
-
|
|
3466
3489
|
// Map the ?active= query to a collections.list filter: 1/true →
|
|
3467
3490
|
// active-only, 0/false → archived-only, absent → all.
|
|
3468
3491
|
function _collectionsFilter(activeS) {
|
|
@@ -3545,11 +3568,11 @@ function mount(router, deps) {
|
|
|
3545
3568
|
});
|
|
3546
3569
|
}
|
|
3547
3570
|
} catch (e) {
|
|
3548
|
-
|
|
3571
|
+
var n = _safeNotice(e, "collection.create");
|
|
3549
3572
|
var rows = await _listForBrowser({});
|
|
3550
|
-
return _sendHtml(res,
|
|
3573
|
+
return _sendHtml(res, n.status, renderAdminCollections({
|
|
3551
3574
|
shop_name: deps.shop_name, nav_available: navAvailable, collections: rows,
|
|
3552
|
-
notice:
|
|
3575
|
+
notice: n.message.replace(/^collections[.:]\s*/, ""), form_type: type,
|
|
3553
3576
|
}));
|
|
3554
3577
|
}
|
|
3555
3578
|
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".collection.create", outcome: "success" });
|
|
@@ -3863,11 +3886,11 @@ function mount(router, deps) {
|
|
|
3863
3886
|
try {
|
|
3864
3887
|
await announcements.defineAnnouncement(_announcementFromForm(req.body || {}));
|
|
3865
3888
|
} catch (e) {
|
|
3866
|
-
|
|
3889
|
+
var n = _safeNotice(e, "announcement.define");
|
|
3867
3890
|
var rows = await announcements.listAnnouncements({});
|
|
3868
|
-
return _sendHtml(res,
|
|
3891
|
+
return _sendHtml(res, n.status, renderAdminAnnouncements({
|
|
3869
3892
|
shop_name: deps.shop_name, nav_available: navAvailable, announcements: rows,
|
|
3870
|
-
notice:
|
|
3893
|
+
notice: n.message.replace(/^announcementBar[.:]\s*/, ""),
|
|
3871
3894
|
}));
|
|
3872
3895
|
}
|
|
3873
3896
|
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".announcement.define", outcome: "success" });
|
|
@@ -3942,11 +3965,11 @@ function mount(router, deps) {
|
|
|
3942
3965
|
questions: _standardSurveyQuestions(body.kind),
|
|
3943
3966
|
});
|
|
3944
3967
|
} catch (e) {
|
|
3945
|
-
|
|
3968
|
+
var n = _safeNotice(e, "survey.define");
|
|
3946
3969
|
var rows = await surveys.listSurveys({});
|
|
3947
|
-
return _sendHtml(res,
|
|
3970
|
+
return _sendHtml(res, n.status, renderAdminSurveys({
|
|
3948
3971
|
shop_name: deps.shop_name, nav_available: navAvailable, surveys: rows,
|
|
3949
|
-
notice:
|
|
3972
|
+
notice: n.message.replace(/^customerSurveys[.:]\s*/, ""),
|
|
3950
3973
|
}));
|
|
3951
3974
|
}
|
|
3952
3975
|
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".survey.define", outcome: "success" });
|
|
@@ -4055,11 +4078,11 @@ function mount(router, deps) {
|
|
|
4055
4078
|
try {
|
|
4056
4079
|
await hours.defineSchedule(_scheduleFromForm(req.body || {}));
|
|
4057
4080
|
} catch (e) {
|
|
4058
|
-
|
|
4081
|
+
var n = _safeNotice(e, "hours.define");
|
|
4059
4082
|
var rows = await hours.listSchedules();
|
|
4060
|
-
return _sendHtml(res,
|
|
4083
|
+
return _sendHtml(res, n.status, renderAdminHours({
|
|
4061
4084
|
shop_name: deps.shop_name, nav_available: navAvailable, schedules: rows,
|
|
4062
|
-
notice:
|
|
4085
|
+
notice: n.message.replace(/^businessHours[.:]\s*/, ""),
|
|
4063
4086
|
}));
|
|
4064
4087
|
}
|
|
4065
4088
|
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".hours.define", outcome: "success" });
|
|
@@ -4240,11 +4263,10 @@ function mount(router, deps) {
|
|
|
4240
4263
|
var win, notice = null;
|
|
4241
4264
|
try { win = _reportWindow(url); }
|
|
4242
4265
|
catch (e) {
|
|
4243
|
-
if (!(e instanceof TypeError)) throw e;
|
|
4244
4266
|
// Bad range → re-render with the default window + a correction
|
|
4245
4267
|
// notice (config/entry tier: the operator fixes the typo).
|
|
4246
4268
|
win = { to: Date.now(), from: Date.now() - b.constants.TIME.days(30) };
|
|
4247
|
-
notice = e.message.replace(/^admin:\s*/, "");
|
|
4269
|
+
notice = _safeNotice(e, "report.view").message.replace(/^admin:\s*/, "");
|
|
4248
4270
|
}
|
|
4249
4271
|
// CSV download from the browser surface too (a link, not a fetch).
|
|
4250
4272
|
if (url && url.searchParams.get("format") === "csv") {
|
|
@@ -4265,9 +4287,8 @@ function mount(router, deps) {
|
|
|
4265
4287
|
var report;
|
|
4266
4288
|
try { report = await _buildReport(win); }
|
|
4267
4289
|
catch (e3) {
|
|
4268
|
-
if (!(e3 instanceof TypeError)) throw e3;
|
|
4269
4290
|
win = { to: Date.now(), from: Date.now() - b.constants.TIME.days(30) };
|
|
4270
|
-
notice = e3.message.replace(/^salesReports:\s*/, "");
|
|
4291
|
+
notice = _safeNotice(e3, "report.view").message.replace(/^salesReports:\s*/, "");
|
|
4271
4292
|
report = await _buildReport(win);
|
|
4272
4293
|
}
|
|
4273
4294
|
_sendHtml(res, 200, renderAdminReports({
|
|
@@ -4467,11 +4488,11 @@ function mount(router, deps) {
|
|
|
4467
4488
|
try {
|
|
4468
4489
|
await subscriptions.plans.create(input);
|
|
4469
4490
|
} catch (e) {
|
|
4470
|
-
|
|
4491
|
+
var n = _safeNotice(e, "subscription_plan.create");
|
|
4471
4492
|
var rows = await subscriptions.plans.list({});
|
|
4472
|
-
return _sendHtml(res,
|
|
4493
|
+
return _sendHtml(res, n.status, renderAdminSubscriptionPlans({
|
|
4473
4494
|
shop_name: deps.shop_name, nav_available: navAvailable, plans: rows,
|
|
4474
|
-
notice:
|
|
4495
|
+
notice: n.message.replace(/^subscriptions[.:]\s*/, ""),
|
|
4475
4496
|
}));
|
|
4476
4497
|
}
|
|
4477
4498
|
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".subscription_plan.create", outcome: "success" });
|
|
@@ -4629,11 +4650,11 @@ function mount(router, deps) {
|
|
|
4629
4650
|
try {
|
|
4630
4651
|
issued = await _issueGiftCard(req.body || {});
|
|
4631
4652
|
} catch (e) {
|
|
4632
|
-
|
|
4653
|
+
var n = _safeNotice(e, "gift_card.issue");
|
|
4633
4654
|
var rows = await giftcards.list({});
|
|
4634
|
-
return _sendHtml(res,
|
|
4655
|
+
return _sendHtml(res, n.status, renderAdminGiftCards({
|
|
4635
4656
|
shop_name: deps.shop_name, nav_available: navAvailable, cards: rows,
|
|
4636
|
-
notice:
|
|
4657
|
+
notice: n.message.replace(/^giftcards?[.:]\s*/i, ""),
|
|
4637
4658
|
}));
|
|
4638
4659
|
}
|
|
4639
4660
|
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".gift_card.issue", outcome: "success", metadata: { id: issued.id } });
|
|
@@ -4821,12 +4842,18 @@ function mount(router, deps) {
|
|
|
4821
4842
|
try {
|
|
4822
4843
|
await taxRates.defineRate(_taxRateInput(body));
|
|
4823
4844
|
} catch (e) {
|
|
4824
|
-
|
|
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*/, "");
|
|
4825
4852
|
var rows = await _taxRatesForBrowser(jurisdiction);
|
|
4826
|
-
return _sendHtml(res,
|
|
4853
|
+
return _sendHtml(res, n.status, renderAdminTaxRates({
|
|
4827
4854
|
shop_name: deps.shop_name, nav_available: navAvailable, rates: rows,
|
|
4828
4855
|
jurisdiction: jurisdiction, sources: taxRates.SOURCES,
|
|
4829
|
-
notice:
|
|
4856
|
+
notice: noticeMsg,
|
|
4830
4857
|
}));
|
|
4831
4858
|
}
|
|
4832
4859
|
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".tax_rate.create", outcome: "success" });
|
|
@@ -5014,14 +5041,21 @@ function mount(router, deps) {
|
|
|
5014
5041
|
var filing;
|
|
5015
5042
|
try { filing = await salesTaxFilings.defineFilingPeriod(_filingPeriodInput(req.body || {})); }
|
|
5016
5043
|
catch (e) {
|
|
5017
|
-
|
|
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*/, "");
|
|
5018
5052
|
var rows = await _filingsForBrowser({});
|
|
5019
5053
|
var upcoming = await _upcomingForBrowser();
|
|
5020
|
-
return _sendHtml(res,
|
|
5054
|
+
return _sendHtml(res, n.status, renderAdminTaxFilings({
|
|
5021
5055
|
shop_name: deps.shop_name, nav_available: navAvailable,
|
|
5022
5056
|
filings: rows, upcoming: upcoming,
|
|
5023
5057
|
kinds: salesTaxFilings.KINDS, statuses: salesTaxFilings.STATUSES,
|
|
5024
|
-
notice:
|
|
5058
|
+
notice: noticeMsg,
|
|
5025
5059
|
}));
|
|
5026
5060
|
}
|
|
5027
5061
|
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".tax_filing.create", outcome: "success", metadata: { id: filing.id } });
|
|
@@ -5064,7 +5098,7 @@ function mount(router, deps) {
|
|
|
5064
5098
|
notice = "Enter both a from and a to date (epoch-ms) for the report window.";
|
|
5065
5099
|
} else {
|
|
5066
5100
|
try { report = await salesTaxFilings.auditReportForJurisdiction({ jurisdiction: jurisdiction, from: from, to: to }); }
|
|
5067
|
-
catch (e) {
|
|
5101
|
+
catch (e) { notice = _safeNotice(e, "tax_filing.report").message.replace(/^salesTaxFilings[.:]\s*/, ""); }
|
|
5068
5102
|
}
|
|
5069
5103
|
_sendHtml(res, 200, renderAdminTaxFilingReport({
|
|
5070
5104
|
shop_name: deps.shop_name, nav_available: navAvailable,
|
|
@@ -5132,10 +5166,13 @@ function mount(router, deps) {
|
|
|
5132
5166
|
try {
|
|
5133
5167
|
await run(req.params.id, req.body || {});
|
|
5134
5168
|
} catch (e) {
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
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*/, "");
|
|
5139
5176
|
return _redirect(res, "/admin/tax-filings/" + enc + "?err=1&err_msg=" + encodeURIComponent(msg));
|
|
5140
5177
|
}
|
|
5141
5178
|
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".tax_filing." + audit, outcome: "success", metadata: { id: req.params.id } });
|
|
@@ -5232,11 +5269,17 @@ function mount(router, deps) {
|
|
|
5232
5269
|
try {
|
|
5233
5270
|
await shippingZones.defineZone(_shippingZoneInput(req.body || {}));
|
|
5234
5271
|
} catch (e) {
|
|
5235
|
-
|
|
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*/, "");
|
|
5236
5279
|
var rows = await shippingZones.listZones({});
|
|
5237
|
-
return _sendHtml(res,
|
|
5280
|
+
return _sendHtml(res, n.status, renderAdminShipping({
|
|
5238
5281
|
shop_name: deps.shop_name, nav_available: navAvailable, zones: rows,
|
|
5239
|
-
notice:
|
|
5282
|
+
notice: noticeMsg,
|
|
5240
5283
|
}));
|
|
5241
5284
|
}
|
|
5242
5285
|
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".shipping_zone.create", outcome: "success" });
|
|
@@ -5430,9 +5473,9 @@ function mount(router, deps) {
|
|
|
5430
5473
|
try {
|
|
5431
5474
|
await autoDiscount.defineRule(_discountInput(req.body || {}));
|
|
5432
5475
|
} catch (e) {
|
|
5433
|
-
|
|
5434
|
-
return _sendHtml(res,
|
|
5435
|
-
notice:
|
|
5476
|
+
var n = _safeNotice(e, "auto_discount.create");
|
|
5477
|
+
return _sendHtml(res, n.status, await _renderDiscounts({
|
|
5478
|
+
notice: n.message.replace(/^autoDiscount[.:]\s*/, ""),
|
|
5436
5479
|
}));
|
|
5437
5480
|
}
|
|
5438
5481
|
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".auto_discount.create", outcome: "success" });
|
|
@@ -5523,9 +5566,9 @@ function mount(router, deps) {
|
|
|
5523
5566
|
try {
|
|
5524
5567
|
await couponStacking.definePolicy(_policyInput(req.body || {}));
|
|
5525
5568
|
} catch (e) {
|
|
5526
|
-
|
|
5527
|
-
return _sendHtml(res,
|
|
5528
|
-
notice:
|
|
5569
|
+
var n = _safeNotice(e, "coupon_policy.create");
|
|
5570
|
+
return _sendHtml(res, n.status, await _renderDiscounts({
|
|
5571
|
+
notice: n.message.replace(/^couponStacking[.:]\s*/, ""),
|
|
5529
5572
|
}));
|
|
5530
5573
|
}
|
|
5531
5574
|
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".coupon_policy.create", outcome: "success" });
|
|
@@ -5639,10 +5682,6 @@ function mount(router, deps) {
|
|
|
5639
5682
|
return input;
|
|
5640
5683
|
}
|
|
5641
5684
|
|
|
5642
|
-
function _qdCleanMessage(e) {
|
|
5643
|
-
return (e && e.message || "Couldn't save that tier set.").replace(/^quantityDiscounts[.:]\s*/, "");
|
|
5644
|
-
}
|
|
5645
|
-
|
|
5646
5685
|
async function _renderQdList(flags) {
|
|
5647
5686
|
flags = flags || {};
|
|
5648
5687
|
var rows = await quantityDiscounts.list(_qdFilter(flags.archived_filter));
|
|
@@ -5682,8 +5721,10 @@ function mount(router, deps) {
|
|
|
5682
5721
|
try {
|
|
5683
5722
|
await quantityDiscounts.defineTier(_qdDefineInput(req.body || {}));
|
|
5684
5723
|
} catch (e) {
|
|
5685
|
-
|
|
5686
|
-
return _sendHtml(res,
|
|
5724
|
+
var n = _safeNotice(e, "quantity_discount.create");
|
|
5725
|
+
return _sendHtml(res, n.status, await _renderQdList({
|
|
5726
|
+
notice: n.message.replace(/^quantityDiscounts[.:]\s*/, ""),
|
|
5727
|
+
}));
|
|
5687
5728
|
}
|
|
5688
5729
|
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".quantity_discount.create", outcome: "success" });
|
|
5689
5730
|
_redirect(res, "/admin/quantity-discounts?created=1");
|
|
@@ -6027,9 +6068,10 @@ function mount(router, deps) {
|
|
|
6027
6068
|
if (values.support_url) await config.put("shop.support_url", values.support_url);
|
|
6028
6069
|
await config.put("setup.completed", true);
|
|
6029
6070
|
} catch (e) {
|
|
6030
|
-
|
|
6071
|
+
var n = _safeNotice(e, "setup.save");
|
|
6072
|
+
return _sendHtml(res, n.status, renderAdminSetup({
|
|
6031
6073
|
shop_name: deps.shop_name, values: values, nav_available: navAvailable,
|
|
6032
|
-
notice:
|
|
6074
|
+
notice: n.message,
|
|
6033
6075
|
}));
|
|
6034
6076
|
}
|
|
6035
6077
|
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".setup.save", outcome: "success", metadata: {} });
|
package/lib/asset-manifest.json
CHANGED
|
@@ -142,6 +142,33 @@ function clientKey(req) {
|
|
|
142
142
|
return sock || "unknown";
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Build the security-headers options for createApp's
|
|
147
|
+
* `middleware.securityHeaders`. The vendored blamejs default emits a
|
|
148
|
+
* `Document-Policy` header asserting `document-write=?0`,
|
|
149
|
+
* `unsized-media=?0`, and `oversized-images=?0`. Current Chromium
|
|
150
|
+
* recognizes none of those three feature names — it parses the header,
|
|
151
|
+
* rejects every token, logs "Unrecognized document policy feature name
|
|
152
|
+
* <x>" to the console, and applies nothing. The header is therefore
|
|
153
|
+
* inert: it adds console noise on every container response while
|
|
154
|
+
* enforcing no policy. (The recognized Document-Policy feature set today
|
|
155
|
+
* is `force-load-at-top` / `js-profiling` /
|
|
156
|
+
* `include-js-call-stacks-in-crash-reports` / `expect-no-linked-resources`
|
|
157
|
+
* / `network-efficiency-guardrails` — none of which is a control this
|
|
158
|
+
* storefront needs to assert.) We have no valid, useful Document-Policy
|
|
159
|
+
* to send, so disable the header rather than ship one the browser
|
|
160
|
+
* rejects. This also matches the edge: the Worker's `_SECURITY_HEADERS`
|
|
161
|
+
* set (worker/index.js) emits no Document-Policy, so suppressing it on
|
|
162
|
+
* the container makes the two substrates header-consistent. Every other
|
|
163
|
+
* vendored default (HSTS, CSP, Permissions-Policy, COOP/CORP, X-Frame-
|
|
164
|
+
* Options, etc.) stays ON. Pass-through to the framework primitive — we
|
|
165
|
+
* compose its `documentPolicy: false` override, never patch the vendored
|
|
166
|
+
* tree.
|
|
167
|
+
*/
|
|
168
|
+
function securityHeadersOpts() {
|
|
169
|
+
return { documentPolicy: false };
|
|
170
|
+
}
|
|
171
|
+
|
|
145
172
|
/**
|
|
146
173
|
* Build the GLOBAL rate-limit options for createApp's
|
|
147
174
|
* `middleware.rateLimit`. Token-bucket so a bursty-but-bounded browsing
|
|
@@ -282,6 +309,7 @@ function mountRouteGuards(r) {
|
|
|
282
309
|
|
|
283
310
|
module.exports = {
|
|
284
311
|
clientKey: clientKey,
|
|
312
|
+
securityHeadersOpts: securityHeadersOpts,
|
|
285
313
|
globalRateLimitOpts: globalRateLimitOpts,
|
|
286
314
|
mountRouteGuards: mountRouteGuards,
|
|
287
315
|
WEBHOOK_PATHS: WEBHOOK_PATHS,
|
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.7",
|
|
7
|
+
"tag": "v0.14.7",
|
|
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.7 (2026-05-30) — **Storage and audit-trail hardening: queries are gated to declared columns, raw SQL refuses embedded literals, the database key is bound to its location, sealed-column lookup hashes gain a keyed mode, audit-chain purges can require dual control, and breach deadlines ship a running clock.** This release tightens the data and audit layers against a set of failure modes that were previously reachable. Database queries are now checked against the columns a table declared in its schema: a reference to an undeclared column fails closed by default instead of silently matching nothing, and the `whereRaw` escape hatch refuses an embedded string literal so values bind through placeholders. The database encryption key is sealed with its purpose, data directory, and key path as additional authenticated data, so a key file cannot be relocated to another deployment and unsealed there; an older key without that binding upgrades itself on first load. Sealed-column equality-lookup hashes can now be computed as a keyed MAC (HMAC-SHAKE256) off a per-deployment key, making the lookup hash unforgeable without that key, while the salted-SHA3 default is unchanged. Purging the tamper-evident audit chain can be placed under a two-authorizer dual-control grant so one operator cannot erase it alone, and database credential-rejection audits now record which relation the rejected credential tried to reach. Finally, breach-notification deadlines get a running clock that raises approaching and passed alerts as each regime's window elapses. One behavior change to note: the column gate defaults to reject — if a service issues queries against columns it did not declare in its schema, set `db.init({ columnGate: "warn" })` (audited, allowed) or `"off"` while the schema is reconciled. **Added:** *Column-membership gate on every query* — `b.db.from(table)` now checks each referenced column against the table's declared schema. The mode is set with `db.init({ columnGate: "reject" | "warn" | "off" })` (default `reject`), and `query.allowedColumns([...])` narrows a single query to an explicit allowlist that is always enforced. `b.db.getDeclaredColumns(table)` returns a table's declared column names (or `null` for an unknown table). This is defense in depth against typo'd or caller-influenced column names reaching the SQL layer (CWE-89). · *Keyed mode for sealed-column lookup hashes* — Equality-lookup ("derived") hashes for sealed columns can be computed as `hmac-shake256` — a keyed MAC over a per-deployment key — instead of the default `salted-sha3`. Set it per table with `cryptoField.registerTable(name, { derivedHashMode: "hmac-shake256" })` or per column with `{ from, mode: "hmac-shake256" }`. The keyed hash is unforgeable and un-correlatable without the deployment's MAC key, which raises the bar against offline lookup-table attacks on low-entropy sealed values (CWE-916). `b.vault.getDerivedHashMacKey()` exposes the 32-byte per-deployment key; it is created on first use and re-sealed across key rotation automatically. · *Dual-control gate on audit-chain purge* — `b.auditTools.purge` accepts `dualControlGrant`. When `audit_log` is placed under dual control, a verified archive and `confirm: true` are no longer sufficient: the purge additionally requires a consumed m-of-n grant whose action is bound to the purge, so a grant minted for another operation cannot be replayed and one operator cannot erase the tamper-evident chain alone (NIST SP 800-53 AU-9, separation of duties). · *Running clock for breach-notification deadlines* — `b.incident.report.createDeadlineClock({ notify, approachThresholds })` tracks open incidents and raises `deadline_approaching` and `deadline_passed` alerts as each regime's window elapses (GDPR 72h, DORA, NIS2, and the rest of the registry). Alerts are deduplicated per incident and stage, suppressed once a submission stage is acknowledged, and the clock can run on an interval or be ticked manually. **Changed:** *Queries against undeclared columns now fail closed by default* — The column gate defaults to `reject`: a query that references a column the table did not declare throws rather than silently matching nothing. A service that intentionally queries undeclared columns can set `db.init({ columnGate: "warn" })` to audit and allow, or `"off"` to disable the gate, while its schema is reconciled. Framework-declared columns (including `_id` and derived-hash columns) are always members. **Security:** *Database encryption key bound to its location* — `db.key.enc` is sealed with additional authenticated data over its purpose, resolved data directory, and resolved key path. A sealed key copied to a different deployment or path no longer unseals there — the AEAD authentication fails — which prevents silent key relocation. A legacy key sealed without this binding is detected and re-sealed in the bound format on first load, with no operator action required. · *`whereRaw` refuses embedded string literals* — `whereRaw(sql, params)` and `WhereBuilder.raw(sql, params)` reject a raw fragment containing a string literal (`'...'`); values must bind through the `params` array. A static, operator-controlled literal can opt in with `{ allowLiterals: true }`. This closes a path where a value concatenated into a raw fragment would reintroduce SQL injection (CWE-89). · *Credential-rejection audits record the attempted relation* — A `db.auth.failed` audit row (SQLSTATE 28000 / 28P01 / 42501) now carries `attemptedTable`, the relation the rejected credential tried to reach, extracted defensively from the statement. Triage can scope the blast radius of a credential-abuse event without correlating back to the raw SQL log (CWE-778). **Detectors:** *Audit-purge dual-control gate* — A new check fails the build if a call to `purgeAuditChain` appears in a file that does not also route through the dual-control gate, so a future caller cannot physically delete chain rows without two-authorizer enforcement. · *Raw-SQL literal/interpolation guard* — A new check fails the build on a `whereRaw` / `.raw` call whose SQL argument is built by template interpolation or string concatenation, keeping the bound-params discipline enforceable in framework code. · *Hand-rolled lookup-hash guard* — A new check fails the build if a sealed-column lookup hash is derived from the per-deployment salt outside the canonical helper, so call sites cannot bypass the keyed-mode and per-column mode policy. · *Auth-audit attempted-relation guard* — A new check fails the build if a `db.auth.failed` audit is emitted in a file that does not name `attemptedTable`, so the forensic field cannot be dropped from a future emitter.
|
|
12
|
+
|
|
11
13
|
- v0.14.6 (2026-05-30) — **Access-refusal middleware can return RFC 9457 problem+json or a custom response, and several documented-but-uncallable APIs are now reachable.** Every access-refusal middleware — the auth gates (bearer, DPoP, mTLS, AAL, bound-key), CSRF, CORS, rate-limit, bot-guard, age-gate, the host and network allowlists, and the method and content-type gates — now accepts two uniform options: `problemDetails: true` returns an RFC 9457 `application/problem+json` body, and `onDeny(req, res, info)` hands the response to the caller. With neither set the refusal is byte-for-byte what it was, so this is a drop-in change that lets a service standardize one error envelope across its API instead of working around each middleware's hardcoded body. Alongside that: `b.middleware.requireBoundKey` is now exported (it was documented and tested but never wired into the middleware surface), `b.middleware.bearerAuth` accepts `requiredScopes` (previously rejected at construction, which made its scope-enforcement path unreachable), API-key refusals send the RFC 6750 challenge code that matches the failure, two documented call paths that named a missing namespace segment are corrected, and the release flow now flags stale GitHub Actions and vendored bundles — with a ready-to-paste pin — before a dependency PR is needed. **Added:** *Uniform `onDeny` and `problemDetails` options on every access-refusal middleware* — Each request-lifecycle middleware that refuses a request now takes `problemDetails: true` to emit an RFC 9457 `application/problem+json` body (composing `b.problemDetails`) and `onDeny(req, res, info)` to take over the response entirely; `info` carries the status, a machine reason, and the middleware-specific fields. The deny-path response headers (`Allow`, `WWW-Authenticate`, `Retry-After`, `Accept`) survive every mode. When neither option is set the response is unchanged. Covers `requireAuth`, `requireAal`, `requireMethods`, `requireContentType`, `requireMtls`, `requireBoundKey`, `bearerAuth`, `dpop`, `csrfProtect`, `fetchMetadata`, `botGuard`, `ageGate`, `hostAllowlist`, `networkAllowlist`, `cors`, `rateLimit`, and `dailyByteQuota` (whose existing `onExceeded` keeps working as an alias of `onDeny`). **Fixed:** *`b.middleware.requireBoundKey` is now callable* — The Bearer-API-key middleware was documented (with examples and tests) but never exported on `b.middleware`, so `b.middleware.requireBoundKey(...)` threw `undefined is not a function`. It is now wired into the middleware surface. · *`b.middleware.bearerAuth` accepts `requiredScopes`* — The RFC 6750 scope-enforcement path read `opts.requiredScopes`, but the option was rejected at construction with `unknown option`, making the 403 `insufficient_scope` behavior unreachable. `requiredScopes` is now an accepted option. · *RFC 6750 challenge codes on API-key refusals* — `b.middleware.requireBoundKey` now sends the `WWW-Authenticate` error code that matches the failure: `insufficient_scope` on a 403 missing-scope, `invalid_token` on an unknown or revoked token, and no error code on a 401 that presented no credentials (RFC 6750 §3). It previously sent `invalid_request` for every refusal. · *Corrected two documented call paths* — The compliance and network references named a path that dropped a namespace segment: the conformity-assessment scaffold is at `b.cra.report.conformityAssessment` (not `b.cra.conformityAssessment`), and the per-socket tuning helper is at `b.network.socket.applyToSocket` (not `b.network.applyToSocket`). The documented signatures now match the callable paths. · *GitHub Actions pins refreshed* — `github/codeql-action` 4.35.5 to 4.36.0, and `docker/login-action`, `docker/setup-buildx-action`, and `docker/setup-qemu-action` to their latest releases. **Detectors:** *`@primitive` reachability gate* — A new check resolves every documented `b.X.Y` primitive against the actual public surface and fails the build when a documented path is not callable (factory-instance shorthands excluded). This is the gate that would have caught the `requireBoundKey` and call-path issues above. · *Deny-path composition gate* — A new check requires every access-refusal middleware to route its refusal through the shared deny-response writer, so a future middleware cannot reintroduce a hardcoded body that locks callers out of `onDeny` / `problemDetails`. · *Actions and vendor currency in the release flow* — The release flow now fails the cut when a SHA-pinned GitHub Action or a vendored bundle is behind its latest upstream release. The actions report prints a ready-to-paste `owner/repo@<sha> # vX.Y.Z` pin and every file and line that uses it, so the bump is copy-paste rather than an after-the-fact dependency PR. Transient registry or API errors stay advisory so a flaky network response does not block an unrelated release.
|
|
12
14
|
|
|
13
15
|
- v0.14.5 (2026-05-30) — **Finished cleaning up the mislabeled byte-literal lint suppressions, with no API or behavior changes.** A follow-up to the byte-literal lint tightening. The remaining suppression comments that named the byte-literal check on values that are not byte sizes — JSON-RPC error codes, HTTP status codes, octet ranges, day-in-milliseconds constants — are removed, keeping their explanatory text and any correctly-named companion suppression. Every byte-literal suppression that remains is now on genuine 1024-scale byte arithmetic. Source-comment hygiene only. **Changed:** *Remaining mislabeled byte-literal suppressions removed* — The byte-literal lint was previously a check on any multiple-of-8 integer, so suppression comments naming it were scattered across non-byte values. The last of those (in a handful of files, in mixed comment formats) are now removed — their explanatory text is retained as plain comments, and any correctly-named companion suppression is kept. The only byte-literal suppressions that remain are on genuine 1024-scale byte arithmetic. No change to any exported API, error code, wire format, or runtime behavior.
|