@blamejs/blamejs-shop 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/README.md +1 -1
- package/lib/admin.js +274 -7
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +14 -0
- package/lib/vendor/blamejs/README.md +6 -5
- package/lib/vendor/blamejs/SECURITY.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +166 -3
- package/lib/vendor/blamejs/lib/cose.js +284 -10
- package/lib/vendor/blamejs/lib/crypto.js +119 -0
- package/lib/vendor/blamejs/lib/did.js +69 -20
- package/lib/vendor/blamejs/lib/mdoc.js +122 -0
- package/lib/vendor/blamejs/lib/network-dnssec.js +328 -0
- package/lib/vendor/blamejs/lib/network.js +1 -0
- package/lib/vendor/blamejs/lib/vc.js +231 -33
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.12.42.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.43.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.44.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.45.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.46.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.47.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.48.json +22 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +38 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/cose.test.js +101 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-self-test.test.js +74 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/did.test.js +29 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dnssec.test.js +130 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mdoc.test.js +52 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vc.test.js +63 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.1.x
|
|
10
10
|
|
|
11
|
+
- v0.1.1 (2026-05-25) — **Admin setup wizard + a browser-accessible admin console.** The admin is now reachable from a browser, not just the bearer-token JSON API. Sign in once at /admin by pasting the ADMIN_API_KEY and a sealed, /admin-scoped session cookie carries you through the guided setup wizard (shop name, contact email, default currency, support URL — saved to shop config) and the analytics dashboard. The shop name set in the wizard drives the storefront header, page titles, and the admin header. **Added:** *Browser admin sign-in* — `/admin` renders a sign-in form; pasting the ADMIN_API_KEY sets a sealed `shop_admin` session cookie (SameSite=Strict, scoped to /admin) so the rendered admin pages are reachable from a browser. The JSON API stays bearer-only; the dashboard accepts either the cookie or the bearer token. · *Setup wizard* — `/admin/setup` is a guided form for the shop's core identity — name, contact email, default currency, support URL — validated (ISO-4217 currency, RFC-shaped email, http(s) support URL) and saved to shop config. The landing nags until setup is complete; the shop name then drives the storefront and admin headers.
|
|
12
|
+
|
|
11
13
|
- v0.1.0 (2026-05-25) — **Responsive cart + storefront cookie handling moved onto the framework primitive.** A storefront polish pass. The cart, order-confirmation, and account-history tables now reflow into stacked, labelled cards on phones and fit their column on wider screens — no more inner horizontal scroll. The quantity field reads clearly and accepts up to 99,999. The line actions (Update / Save for later / Remove) are compact with a clear hierarchy. Under the hood, all storefront cookie handling now composes the framework's cookie primitive (RFC 6265 parse/serialize plus vault-sealed read/write) instead of hand-built headers, and a malformed session cookie can no longer turn the cart — or any page that shows the cart count — into a 500. **Changed:** *Quantity field is readable and accepts up to 99,999* — The cart quantity input is wide enough to read a five-digit quantity, and the per-line maximum is raised to 99,999 (enforced on the server). · *Compact, clearly-ranked line actions* — Update is a small accent button, Save for later a quiet secondary, Remove a danger-tinted secondary — so the primary checkout call stays dominant. · *Cookie handling composes the framework primitive* — Session, authentication, WebAuthn-challenge, and payment cookies are all parsed and written through the framework cookie primitive (sealed read/write for the authenticated-session cookie), replacing hand-built Set-Cookie strings and manual header parsing. Cookie lifetimes are expressed through the framework's duration constants. **Fixed:** *Cart tables no longer scroll sideways* — The cart, order-confirmation, and account order-history tables reflow into one labelled card per row below 48rem and are sized to fit their column on wider layouts, so content is never trapped behind an inner horizontal scrollbar. · *Malformed session cookie no longer 500s the storefront* — A `shop_sid` cookie carrying a value that isn't a well-formed session id now reads as "no session" instead of reaching the cart lookup (which rejected it and surfaced a 500 on every page that renders the cart count). · *Account dashboard controls wrap on narrow screens* — The row of account actions (Wishlist / Saved for later / Recently viewed / Addresses / Returns / Sign out) now wraps instead of overflowing the viewport on a phone.
|
|
12
14
|
|
|
13
15
|
## v0.0.x
|
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ Every primitive is composed on the vendored blamejs surface — no npm runtime d
|
|
|
72
72
|
| **`lib/collections.js`** | Curated + smart product groupings. `GET /collections` lists the shop's active collections; `GET /collections/:slug` renders the grid — manual collections list hand-picked members, smart collections evaluate stored rules against the active catalog and apply the collection's sort strategy. Each product resolves fresh, so archived products drop out. Public, no sign-in; a bad or unknown slug is a 404 (never a 500). Linked from the footer on every page. |
|
|
73
73
|
| **`lib/subscriptions.js`** | Stripe-backed recurring billing — `subscription_plans` (interval / amount / trial) + `subscriptions` (mirrors Stripe's object byte-for-byte). `subscriptions.create` POSTs to Stripe via the payment dep, then persists the returned object locally. `handleStripeEvent` replays `customer.subscription.*` events into the local row so the shop has an authoritative view without round-tripping. |
|
|
74
74
|
| **`lib/newsletter.js`** | Operator-collected email broadcast list — `signup({ email, source })` composes `b.guardEmail` for shape validation, `b.crypto.namespaceHash` for the dedup key, and `INSERT OR IGNORE` for idempotency. Storefront POST `/newsletter` route renders a designed thank-you card with separate copy for the `new` vs `dedup` branches. |
|
|
75
|
-
| **`lib/admin.js`** | Bearer-token-gated CRUD over catalog + orders + refunds + bulk CSV import + subscription plans + review moderation + return moderation. Token compared via `b.crypto.timingSafeEqual`. Errors as RFC 9457 problem documents via `b.problemDetails`. Audit emission on every mutation. |
|
|
75
|
+
| **`lib/admin.js`** | Bearer-token-gated CRUD over catalog + orders + refunds + bulk CSV import + subscription plans + review moderation + return moderation. Token compared via `b.crypto.timingSafeEqual`. Errors as RFC 9457 problem documents via `b.problemDetails`. Audit emission on every mutation. Also serves a **browser admin console**: sign in at `/admin` by pasting the API key (sealed `shop_admin` session cookie, SameSite=Strict, /admin-scoped), a guided **setup wizard** at `/admin/setup` that writes shop identity to config, and the analytics dashboard — all reachable by the cookie or the bearer token. |
|
|
76
76
|
| **`lib/catalog-import.js`** | Bulk CSV import — `POST /admin/catalog/import` accepts a `text/csv` body, parses via `b.csv`, content-safety-filters every cell through `b.guardCsv` (formula-injection / bidi / control / dangerous-function denylist), validates exact header order, de-dupes rows by `product_slug`, returns per-row errors without aborting. Default 1 MiB / 10000 rows caps. |
|
|
77
77
|
| **`lib/theme.js`** | File-backed templates with fallback chain. Operators register a named theme under `<themesDir>/<name>/*.html` and the storefront dispatches every renderer through it. `assetUrl(path)` resolves to `/assets/themes/<name>/<path>`. The shipped `default` theme is the fallback. |
|
|
78
78
|
|
package/lib/admin.js
CHANGED
|
@@ -105,6 +105,53 @@ function _authOk(token, expected) {
|
|
|
105
105
|
return _b().crypto.timingSafeEqual(token, expected);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// ---- admin browser session (sealed cookie) ------------------------------
|
|
109
|
+
//
|
|
110
|
+
// The JSON API (R/W wrappers) is bearer-token only — that's the contract
|
|
111
|
+
// for machine clients. The server-rendered admin pages (landing, setup
|
|
112
|
+
// wizard, dashboard) additionally accept a sealed `shop_admin` cookie so
|
|
113
|
+
// an operator can sign in from a browser by pasting the same token once.
|
|
114
|
+
// The cookie composes the framework cookie primitive (vault-sealed
|
|
115
|
+
// read/write) — scoped to /admin, SameSite=Strict, HttpOnly + Secure.
|
|
116
|
+
var ADMIN_COOKIE_NAME = "shop_admin";
|
|
117
|
+
|
|
118
|
+
var _adminJarMemo = null;
|
|
119
|
+
function _adminJar() {
|
|
120
|
+
if (!_adminJarMemo) {
|
|
121
|
+
_adminJarMemo = _b().cookies.create({
|
|
122
|
+
vault: _b().vault,
|
|
123
|
+
defaults: { httpOnly: true, secure: true, sameSite: "Strict", path: "/admin" },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return _adminJarMemo;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function _setAdminCookie(res) {
|
|
130
|
+
_adminJar().writeSealed(res, ADMIN_COOKIE_NAME, JSON.stringify({
|
|
131
|
+
admin: true,
|
|
132
|
+
exp: Date.now() + _b().constants.TIME.hours(12),
|
|
133
|
+
}), { expires: new Date(Date.now() + _b().constants.TIME.hours(12)) });
|
|
134
|
+
}
|
|
135
|
+
function _clearAdminCookie(res) {
|
|
136
|
+
_adminJar().clear(res, ADMIN_COOKIE_NAME);
|
|
137
|
+
}
|
|
138
|
+
function _adminCookieValid(req) {
|
|
139
|
+
var raw = _adminJar().readSealed(req, ADMIN_COOKIE_NAME);
|
|
140
|
+
if (raw === null) return false;
|
|
141
|
+
var env;
|
|
142
|
+
try { env = JSON.parse(raw); } catch (_e) { return false; }
|
|
143
|
+
return !!(env && env.admin === true && env.exp && env.exp > Date.now());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// HTML-page auth: a valid admin cookie OR the bearer token (so existing
|
|
147
|
+
// tooling that sends the header still reaches the dashboard). Never
|
|
148
|
+
// throws — a missing vault surfaces as "not authed" so the caller can
|
|
149
|
+
// render the login form rather than 500.
|
|
150
|
+
function _htmlAuthed(req, expectedToken) {
|
|
151
|
+
if (_authOk(_readBearer(req), expectedToken)) return true;
|
|
152
|
+
try { return _adminCookieValid(req); } catch (_e) { return false; }
|
|
153
|
+
}
|
|
154
|
+
|
|
108
155
|
function _problem(res, status, code, detail) {
|
|
109
156
|
return _b().problemDetails.send(res, {
|
|
110
157
|
type: "/problems/" + code,
|
|
@@ -114,6 +161,21 @@ function _problem(res, status, code, detail) {
|
|
|
114
161
|
});
|
|
115
162
|
}
|
|
116
163
|
|
|
164
|
+
function _sendHtml(res, status, html) {
|
|
165
|
+
res.status(status);
|
|
166
|
+
if (res.setHeader) {
|
|
167
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
168
|
+
res.setHeader("x-robots-tag", "noindex, nofollow");
|
|
169
|
+
}
|
|
170
|
+
if (res.end) res.end(html); else res.send(html);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function _redirect(res, location) {
|
|
174
|
+
res.status(303);
|
|
175
|
+
if (res.setHeader) res.setHeader("location", location);
|
|
176
|
+
if (res.end) res.end(); else res.send("");
|
|
177
|
+
}
|
|
178
|
+
|
|
117
179
|
function _wrap(handler, opts) {
|
|
118
180
|
// Every admin handler routes through this wrapper: bearer-token
|
|
119
181
|
// gate, error-to-problem-details translation, audit write on the
|
|
@@ -813,24 +875,26 @@ function mount(router, deps) {
|
|
|
813
875
|
_json(res, 200, { rows: rows });
|
|
814
876
|
}));
|
|
815
877
|
|
|
816
|
-
|
|
878
|
+
// HTML page — accepts the admin browser cookie OR the bearer token,
|
|
879
|
+
// so it's reachable both from a signed-in browser and from tooling.
|
|
880
|
+
router.get("/admin/dashboard", async function (req, res) {
|
|
881
|
+
if (!_htmlAuthed(req, expectedToken)) {
|
|
882
|
+
return _sendHtml(res, 200, renderAdminLogin({ shop_name: deps.shop_name }));
|
|
883
|
+
}
|
|
817
884
|
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
818
885
|
var w = _parseWindow(url);
|
|
819
886
|
var summary = await analytics.summary(w);
|
|
820
887
|
var byDay = await analytics.revenueByDay(w);
|
|
821
888
|
var top = await analytics.topSKUs(Object.assign({}, w, { limit: 10 }));
|
|
822
889
|
var recent = await analytics.recentOrders({ limit: 20 });
|
|
823
|
-
|
|
890
|
+
_sendHtml(res, 200, renderDashboard({
|
|
824
891
|
summary: summary,
|
|
825
892
|
by_day: byDay,
|
|
826
893
|
top_skus: top,
|
|
827
894
|
recent: recent,
|
|
828
895
|
shop_name: (deps.shop_name || "blamejs.shop"),
|
|
829
|
-
});
|
|
830
|
-
|
|
831
|
-
if (res.setHeader) res.setHeader("content-type", "text/html; charset=utf-8");
|
|
832
|
-
if (res.end) res.end(html); else res.send(html);
|
|
833
|
-
}));
|
|
896
|
+
}));
|
|
897
|
+
});
|
|
834
898
|
}
|
|
835
899
|
|
|
836
900
|
// ---- subscriptions --------------------------------------------------
|
|
@@ -900,6 +964,112 @@ function mount(router, deps) {
|
|
|
900
964
|
}));
|
|
901
965
|
}
|
|
902
966
|
|
|
967
|
+
// ---- admin web pages (browser session + setup wizard) ---------------
|
|
968
|
+
//
|
|
969
|
+
// The operator signs in by pasting the ADMIN_API_KEY once; that sets a
|
|
970
|
+
// sealed, SameSite=Strict, /admin-scoped cookie so the rendered pages
|
|
971
|
+
// (landing, dashboard, setup) are reachable from a browser. The JSON
|
|
972
|
+
// API stays bearer-only. POST routes follow the storefront's
|
|
973
|
+
// form-POST pattern (SameSite cookie + the app-level origin /
|
|
974
|
+
// fetch-metadata guards), no separate CSRF token field.
|
|
975
|
+
|
|
976
|
+
async function _setupComplete() {
|
|
977
|
+
if (!config) return false;
|
|
978
|
+
try { return (await config.get("setup.completed", false)) === true; }
|
|
979
|
+
catch (_e) { return false; }
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
router.get("/admin", async function (req, res) {
|
|
983
|
+
if (!_htmlAuthed(req, expectedToken)) {
|
|
984
|
+
return _sendHtml(res, 200, renderAdminLogin({ shop_name: deps.shop_name }));
|
|
985
|
+
}
|
|
986
|
+
_sendHtml(res, 200, renderAdminLanding({
|
|
987
|
+
shop_name: deps.shop_name,
|
|
988
|
+
setup_complete: await _setupComplete(),
|
|
989
|
+
}));
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
router.post("/admin/login", async function (req, res) {
|
|
993
|
+
var body = req.body || {};
|
|
994
|
+
var token = typeof body.token === "string" ? body.token : "";
|
|
995
|
+
if (!_authOk(token, expectedToken)) {
|
|
996
|
+
return _sendHtml(res, 401, renderAdminLogin({ shop_name: deps.shop_name, error: true }));
|
|
997
|
+
}
|
|
998
|
+
try { _setAdminCookie(res); }
|
|
999
|
+
catch (e) {
|
|
1000
|
+
// Sealed cookies need an initialized vault — surface 503 rather
|
|
1001
|
+
// than 500 so the operator knows to configure VAULT_PASSPHRASE.
|
|
1002
|
+
if (e && e.code === "vault/not-initialized") {
|
|
1003
|
+
return _sendHtml(res, 503, renderAdminLogin({ shop_name: deps.shop_name }));
|
|
1004
|
+
}
|
|
1005
|
+
throw e;
|
|
1006
|
+
}
|
|
1007
|
+
_redirect(res, (await _setupComplete()) ? "/admin" : "/admin/setup");
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
router.post("/admin/logout", async function (_req, res) {
|
|
1011
|
+
_clearAdminCookie(res);
|
|
1012
|
+
_redirect(res, "/admin");
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
if (config) {
|
|
1016
|
+
router.get("/admin/setup", async function (req, res) {
|
|
1017
|
+
if (!_htmlAuthed(req, expectedToken)) return _redirect(res, "/admin");
|
|
1018
|
+
var url = req.url ? new URL(req.url, "http://localhost") : null;
|
|
1019
|
+
var saved = !!(url && url.searchParams.get("saved"));
|
|
1020
|
+
var values = {};
|
|
1021
|
+
try {
|
|
1022
|
+
values.shop_name = await config.get("shop.name", deps.shop_name || "");
|
|
1023
|
+
values.contact_email = await config.get("shop.contact_email", "");
|
|
1024
|
+
values.currency = await config.get("shop.currency", "");
|
|
1025
|
+
values.support_url = await config.get("shop.support_url", "");
|
|
1026
|
+
} catch (_e) { /* unconfigured — render an empty form */ }
|
|
1027
|
+
_sendHtml(res, 200, renderAdminSetup({ shop_name: deps.shop_name, values: values, saved: saved }));
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
router.post("/admin/setup", async function (req, res) {
|
|
1031
|
+
if (!_htmlAuthed(req, expectedToken)) return _redirect(res, "/admin");
|
|
1032
|
+
var body = req.body || {};
|
|
1033
|
+
var values = {
|
|
1034
|
+
shop_name: (typeof body.shop_name === "string" ? body.shop_name : "").trim(),
|
|
1035
|
+
contact_email: (typeof body.contact_email === "string" ? body.contact_email : "").trim(),
|
|
1036
|
+
currency: (typeof body.currency === "string" ? body.currency : "").trim().toUpperCase(),
|
|
1037
|
+
support_url: (typeof body.support_url === "string" ? body.support_url : "").trim(),
|
|
1038
|
+
};
|
|
1039
|
+
// Defensive request-shape reader: bad input re-renders the form
|
|
1040
|
+
// with a notice (400), never a 500.
|
|
1041
|
+
var notice = null;
|
|
1042
|
+
if (!values.shop_name) notice = "Shop name is required.";
|
|
1043
|
+
else if (values.shop_name.length > 80) notice = "Shop name is too long (max 80 characters).";
|
|
1044
|
+
else if (values.currency && !/^[A-Z]{3}$/.test(values.currency)) notice = "Currency must be a 3-letter ISO 4217 code (e.g. USD).";
|
|
1045
|
+
else if (values.contact_email) {
|
|
1046
|
+
var emailReport = _b().guardEmail.validate(values.contact_email, { profile: "strict" });
|
|
1047
|
+
if (!emailReport || emailReport.ok === false) notice = "That contact email doesn't look valid.";
|
|
1048
|
+
}
|
|
1049
|
+
if (!notice && values.support_url) {
|
|
1050
|
+
var u = _b().safeUrl.parse(values.support_url);
|
|
1051
|
+
if (!u || (u.protocol !== "https:" && u.protocol !== "http:")) notice = "Support URL must be a valid http(s) URL.";
|
|
1052
|
+
}
|
|
1053
|
+
if (notice) {
|
|
1054
|
+
return _sendHtml(res, 400, renderAdminSetup({ shop_name: deps.shop_name, values: values, notice: notice }));
|
|
1055
|
+
}
|
|
1056
|
+
try {
|
|
1057
|
+
await config.put("shop.name", values.shop_name);
|
|
1058
|
+
if (values.contact_email) await config.put("shop.contact_email", values.contact_email);
|
|
1059
|
+
if (values.currency) await config.put("shop.currency", values.currency);
|
|
1060
|
+
if (values.support_url) await config.put("shop.support_url", values.support_url);
|
|
1061
|
+
await config.put("setup.completed", true);
|
|
1062
|
+
} catch (e) {
|
|
1063
|
+
return _sendHtml(res, 500, renderAdminSetup({
|
|
1064
|
+
shop_name: deps.shop_name, values: values,
|
|
1065
|
+
notice: "Couldn't save — " + ((e && e.message) || "please try again."),
|
|
1066
|
+
}));
|
|
1067
|
+
}
|
|
1068
|
+
_b().audit.safeEmit({ action: AUDIT_NAMESPACE + ".setup.save", outcome: "success", metadata: {} });
|
|
1069
|
+
_redirect(res, "/admin/setup?saved=1");
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
|
|
903
1073
|
// ---- ping (auth check) ----------------------------------------------
|
|
904
1074
|
|
|
905
1075
|
router.get("/admin/ping", R(async function (_req, res) {
|
|
@@ -962,6 +1132,25 @@ var DASHBOARD_LAYOUT =
|
|
|
962
1132
|
" .empty { color:var(--mute); font-style:italic; padding:1rem 0; text-align:center; }\n" +
|
|
963
1133
|
" .meta { color:var(--mute); font-size:.85rem; margin-bottom:1rem; }\n" +
|
|
964
1134
|
" .order-id { font-family:ui-monospace, SFMono-Regular, Menlo, monospace; font-size:.78rem; color:var(--ink-2); }\n" +
|
|
1135
|
+
" .form-field { display:block; margin-bottom:1.1rem; }\n" +
|
|
1136
|
+
" .form-field span { display:block; font-size:.78rem; text-transform:uppercase; letter-spacing:.05em; color:var(--mute); font-weight:600; margin-bottom:.35rem; }\n" +
|
|
1137
|
+
" .form-field input { width:100%; max-width:28rem; padding:.6rem .75rem; border:1px solid var(--hair); border-radius:6px; font:inherit; }\n" +
|
|
1138
|
+
" .form-field input:focus { outline:2px solid var(--accent); outline-offset:1px; border-color:var(--accent); }\n" +
|
|
1139
|
+
" .form-field small { display:block; color:var(--mute); font-size:.78rem; margin-top:.3rem; }\n" +
|
|
1140
|
+
" .btn { display:inline-flex; align-items:center; gap:.4rem; background:var(--accent); color:var(--paper); border:1px solid var(--accent); padding:.6rem 1.1rem; border-radius:6px; font-family:'Montserrat',sans-serif; font-weight:700; font-size:.82rem; letter-spacing:.04em; text-transform:uppercase; text-decoration:none; cursor:pointer; }\n" +
|
|
1141
|
+
" .btn:hover { background:var(--accent-d); border-color:var(--accent-d); }\n" +
|
|
1142
|
+
" .btn--ghost { background:transparent; color:var(--ink); border-color:var(--ink); }\n" +
|
|
1143
|
+
" .btn--ghost:hover { background:var(--ink); color:var(--paper); }\n" +
|
|
1144
|
+
" .nav-cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(14rem,1fr)); gap:1rem; }\n" +
|
|
1145
|
+
" .nav-card { display:block; background:var(--paper); border:1px solid var(--hair); border-radius:8px; padding:1.4rem; text-decoration:none; color:var(--ink); }\n" +
|
|
1146
|
+
" .nav-card:hover { border-color:var(--accent); box-shadow:0 8px 20px -12px rgba(0,0,0,.25); }\n" +
|
|
1147
|
+
" .nav-card h3 { margin:0 0 .35rem; font-size:1.05rem; }\n" +
|
|
1148
|
+
" .nav-card p { margin:0; color:var(--mute); font-size:.88rem; }\n" +
|
|
1149
|
+
" .banner { padding:.9rem 1.1rem; border-radius:8px; margin-bottom:1.5rem; font-size:.92rem; }\n" +
|
|
1150
|
+
" .banner--warn { background:#fff8e1; border:1px solid #f1e1a8; color:#7a5d0f; }\n" +
|
|
1151
|
+
" .banner--ok { background:#e9f5ec; border:1px solid #bfe1c9; color:#1f6b3a; }\n" +
|
|
1152
|
+
" .banner--err { background:#fff1eb; border:1px solid #f6c5af; color:var(--accent-d); }\n" +
|
|
1153
|
+
" .actions-row { display:flex; gap:.75rem; flex-wrap:wrap; align-items:center; margin-top:1.5rem; }\n" +
|
|
965
1154
|
" </style>\n" +
|
|
966
1155
|
"</head>\n" +
|
|
967
1156
|
"<body>\n" +
|
|
@@ -1112,8 +1301,86 @@ function _statCard(label, value, accent) {
|
|
|
1112
1301
|
"<div class=\"value" + (accent ? " accent" : "") + "\">" + _htmlEscape(value) + "</div></div>";
|
|
1113
1302
|
}
|
|
1114
1303
|
|
|
1304
|
+
// ---- admin web pages (login / landing / setup wizard) -------------------
|
|
1305
|
+
|
|
1306
|
+
function _renderAdminShell(shopName, subtitle, bodyHtml) {
|
|
1307
|
+
return _renderTemplate(DASHBOARD_LAYOUT, {
|
|
1308
|
+
shop_name: shopName || "blamejs.shop",
|
|
1309
|
+
window_label: subtitle || "",
|
|
1310
|
+
body: "RAW_BODY",
|
|
1311
|
+
}).replace("RAW_BODY", bodyHtml);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function renderAdminLogin(opts) {
|
|
1315
|
+
opts = opts || {};
|
|
1316
|
+
var err = opts.error
|
|
1317
|
+
? "<div class=\"banner banner--err\">That key didn't match. Check the ADMIN_API_KEY this deployment was started with.</div>"
|
|
1318
|
+
: "";
|
|
1319
|
+
var body =
|
|
1320
|
+
"<section style=\"max-width:30rem;\">" +
|
|
1321
|
+
"<h2>Sign in</h2>" + err +
|
|
1322
|
+
"<form method=\"post\" action=\"/admin/login\">" +
|
|
1323
|
+
"<label class=\"form-field\"><span>Admin API key</span>" +
|
|
1324
|
+
"<input type=\"password\" name=\"token\" autocomplete=\"off\" autofocus required>" +
|
|
1325
|
+
"<small>Paste the ADMIN_API_KEY this deployment was started with.</small>" +
|
|
1326
|
+
"</label>" +
|
|
1327
|
+
"<button type=\"submit\" class=\"btn\">Sign in</button>" +
|
|
1328
|
+
"</form>" +
|
|
1329
|
+
"</section>";
|
|
1330
|
+
return _renderAdminShell(opts.shop_name, "Sign in", body);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function renderAdminLanding(opts) {
|
|
1334
|
+
opts = opts || {};
|
|
1335
|
+
var setupBanner = opts.setup_complete
|
|
1336
|
+
? ""
|
|
1337
|
+
: "<div class=\"banner banner--warn\">Your shop isn't set up yet. <a href=\"/admin/setup\">Finish setup →</a></div>";
|
|
1338
|
+
var body =
|
|
1339
|
+
"<section>" + setupBanner +
|
|
1340
|
+
"<h2>Admin</h2>" +
|
|
1341
|
+
"<div class=\"nav-cards\">" +
|
|
1342
|
+
"<a class=\"nav-card\" href=\"/admin/setup\"><h3>Setup wizard</h3><p>Shop identity, currency, and contact details.</p></a>" +
|
|
1343
|
+
"<a class=\"nav-card\" href=\"/admin/dashboard\"><h3>Dashboard</h3><p>Sales, revenue, and recent orders at a glance.</p></a>" +
|
|
1344
|
+
"</div>" +
|
|
1345
|
+
"<div class=\"actions-row\"><form method=\"post\" action=\"/admin/logout\"><button type=\"submit\" class=\"btn btn--ghost\">Sign out</button></form></div>" +
|
|
1346
|
+
"</section>";
|
|
1347
|
+
return _renderAdminShell(opts.shop_name, "", body);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function _setupField(label, name, value, type, hint, extra) {
|
|
1351
|
+
return "<label class=\"form-field\"><span>" + _htmlEscape(label) + "</span>" +
|
|
1352
|
+
"<input type=\"" + (type || "text") + "\" name=\"" + _htmlEscape(name) + "\" value=\"" + _htmlEscape(value || "") + "\"" + (extra || "") + ">" +
|
|
1353
|
+
(hint ? "<small>" + _htmlEscape(hint) + "</small>" : "") +
|
|
1354
|
+
"</label>";
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function renderAdminSetup(opts) {
|
|
1358
|
+
opts = opts || {};
|
|
1359
|
+
var v = opts.values || {};
|
|
1360
|
+
var saved = opts.saved ? "<div class=\"banner banner--ok\">Saved. Your shop details are live.</div>" : "";
|
|
1361
|
+
var notice = opts.notice ? "<div class=\"banner banner--err\">" + _htmlEscape(opts.notice) + "</div>" : "";
|
|
1362
|
+
var body =
|
|
1363
|
+
"<section style=\"max-width:34rem;\">" +
|
|
1364
|
+
"<h2>Shop setup</h2>" +
|
|
1365
|
+
"<p class=\"meta\">Set the basics customers see across the storefront. You can change these any time.</p>" +
|
|
1366
|
+
saved + notice +
|
|
1367
|
+
"<form method=\"post\" action=\"/admin/setup\">" +
|
|
1368
|
+
_setupField("Shop name", "shop_name", v.shop_name, "text", "Shown in the header, page titles, and emails.", " maxlength=\"80\" required") +
|
|
1369
|
+
_setupField("Contact email", "contact_email", v.contact_email, "email", "Where customer replies and operational mail land.", " maxlength=\"160\"") +
|
|
1370
|
+
_setupField("Default currency", "currency", v.currency, "text", "3-letter ISO 4217 code (e.g. USD, EUR, GBP).", " maxlength=\"3\" style=\"text-transform:uppercase;max-width:8rem;\"") +
|
|
1371
|
+
_setupField("Support URL", "support_url", v.support_url, "url", "Linked from the storefront footer (help centre, contact page).", " maxlength=\"300\"") +
|
|
1372
|
+
"<div class=\"actions-row\"><button type=\"submit\" class=\"btn\">Save shop details</button>" +
|
|
1373
|
+
"<a class=\"btn btn--ghost\" href=\"/admin\">Back</a></div>" +
|
|
1374
|
+
"</form>" +
|
|
1375
|
+
"</section>";
|
|
1376
|
+
return _renderAdminShell(opts.shop_name, "Setup", body);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1115
1379
|
module.exports = {
|
|
1116
1380
|
mount: mount,
|
|
1117
1381
|
AUDIT_NAMESPACE: AUDIT_NAMESPACE,
|
|
1118
1382
|
renderDashboard: renderDashboard,
|
|
1383
|
+
renderAdminLogin: renderAdminLogin,
|
|
1384
|
+
renderAdminLanding: renderAdminLanding,
|
|
1385
|
+
renderAdminSetup: renderAdminSetup,
|
|
1119
1386
|
};
|
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.12.
|
|
7
|
-
"tag": "v0.12.
|
|
6
|
+
"version": "0.12.48",
|
|
7
|
+
"tag": "v0.12.48",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|
|
@@ -8,6 +8,20 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.12.x
|
|
10
10
|
|
|
11
|
+
- v0.12.48 (2026-05-25) — **`b.network.dns.dnssec` — local DNSSEC signature verification (RFC 4035).** Verify a DNS answer's RRSIG signature yourself instead of trusting the upstream resolver's AD bit. b.network.dns.dnssec.verifyRrset reconstructs the RFC 4034 §3.1.8.1 signed data — the RRSIG RDATA without the signature, followed by the RRset in canonical form (owner names lowercased, RRs ordered by canonical RDATA, the RRSIG's Original TTL) — and checks the signature against the DNSKEY, enforcing the inception / expiration window. Supports RSA/SHA-256 (alg 8), ECDSA P-256/SHA-256 (13), ECDSA P-384/SHA-384 (14), and Ed25519 (15) — the modern deployed set. verifyDs checks a delegation-signer digest against a DNSKEY (SHA-256 / SHA-384) and keyTag computes the RFC 4034 Appendix B key tag. The verification core is what a chain-walker composes; it defends against a compromised or on-path resolver that lies about authentication. **Added:** *`b.network.dns.dnssec.verifyRrset(opts)`* — Verifies an RRSIG over a canonicalised RRset against a DNSKEY. `opts` carries the owner `name`, the RR `type`, the wire-format `rdatas`, the parsed `rrsig` (algorithm / labels / originalTtl / inception / expiration / keyTag / signerName / signature), and the `dnskey` (algorithm + raw public key). The signed data is rebuilt per RFC 4034 §3.1.8.1: the RRSIG prefix (type covered | algorithm | labels | original TTL | expiration | inception | key tag | canonical signer name) followed by each RR in canonical form (lowercased owner | type | class | original TTL | rdlen | rdata), sorted by `Buffer.compare` on the RDATA. The validity window is enforced against `opts.at` (defaults to now; an invalid Date is refused, not treated as now). An RRSIG whose algorithm disagrees with the DNSKEY is refused before any key is built. RR types that embed domain names in their RDATA (NS, CNAME, SOA, MX, SRV, …) need RDATA-internal name-lowercasing this version does not perform, so they are refused with `dnssec/uncanonicalizable-type` rather than mis-validated; the security-critical DNSKEY / DS and the name-free address / text types (A, AAAA, TXT, CAA, TLSA, …) are fully supported. · *`b.network.dns.dnssec.verifyDs(opts)` / `b.network.dns.dnssec.keyTag(dnskeyRdata)`* — `verifyDs` confirms a delegation-signer record matches a DNSKEY: it checks the key tag, then compares the DS digest (SHA-256 type 2 / SHA-384 type 4) against the digest computed over the canonical owner name and the DNSKEY RDATA, constant-time. `keyTag` computes the RFC 4034 Appendix B 16-bit key tag from a DNSKEY's full RDATA — the identifier an RRSIG or DS uses to select the signing key. Together with `verifyRrset` these are the per-RRset building blocks a recursive chain-walk (root → TLD → zone) composes; the chain-walk itself, NSEC / NSEC3 denial-of-existence, and the bundled IANA root trust anchor are not part of this core.
|
|
12
|
+
|
|
13
|
+
- v0.12.47 (2026-05-25) — **`b.cose.mac0` / `b.cose.macVerify0` — COSE_Mac0 (RFC 9052 §6.2).** Completes the COSE message-type set (COSE_Sign1 / COSE_Encrypt0 / COSE_Mac0) with single shared-key MACs. b.cose.mac0 produces a tagged COSE_Mac0 over a payload using HMAC-SHA-256/384/512 (the COSE-standard MAC algorithms; HMAC is symmetric, so its post-quantum strength is preserved). b.cose.macVerify0 recomputes the tag over the MAC_structure and compares it in constant time, with a mandatory algorithm allowlist. Use when both parties hold a shared key — e.g. an ECDH-derived key — and a non-repudiable signature is not wanted; detached payloads are supported (the proximity mdoc device-MAC variant and MACed CWTs are the consumers). Composes b.cbor + the framework's constant-time compare; no new runtime dependency. **Added:** *`b.cose.mac0(payload, opts)` / `b.cose.macVerify0(coseMac0, opts)`* — `mac0` emits a tagged COSE_Mac0 (tag 17) with `alg` (`HMAC-256/256` | `HMAC-384/384` | `HMAC-512/512`) in the protected header and the HMAC tag computed over the MAC_structure `["MAC0", protected, external_aad, payload]`; `detached: true` emits a nil payload. `macVerify0` reads the algorithm from the protected header (must be in the required `opts.algorithms` allowlist), recomputes the tag, and compares it constant-time — a wrong key, tampered tag, or `external_aad` mismatch is refused with `cose/bad-tag`; a detached payload is supplied via `opts.externalPayload`. `external_aad` binds context into the tag.
|
|
14
|
+
|
|
15
|
+
- v0.12.46 (2026-05-25) — **`b.mdoc.verifyDeviceAuth` — ISO 18013-5 mdoc device authentication.** Completes mdoc verification with the holder-binding half (ISO 18013-5 §9.1.3, signature variant). verifyIssuerSigned proves the data is issuer-signed; verifyDeviceAuth proves the presenter controls the device key the issuer bound into the MSO, so a captured issuer-signed document cannot be replayed by anyone else. The device's COSE_Sign1 (deviceSigned.deviceAuth.deviceSignature) is verified over the detached DeviceAuthentication structure ["DeviceAuthentication", SessionTranscript, DocType, DeviceNameSpacesBytes] using the device key from verifyIssuerSigned().deviceKey (now surfaced) and the operator-supplied SessionTranscript that binds the proof to this exact exchange (the presentation protocol — e.g. OpenID4VP — defines the transcript). Composes the v0.12.45 b.cose detached-payload verify + importKey. The MAC variant (deviceMac / COSE_Mac0, used in proximity flows with a reader ephemeral key) is deferred and refused with mdoc/device-mac-unsupported. No new runtime dependency. **Added:** *`b.mdoc.verifyDeviceAuth(opts)` + `deviceKey` on the verifyIssuerSigned result* — `verifyDeviceAuth({ deviceKey, deviceSigned, docType, sessionTranscript, algorithms })` imports the device key (a COSE_Key via `b.cose.importKey`, or a KeyObject), reconstructs the detached `DeviceAuthentication` payload, and verifies the `deviceSignature` COSE_Sign1 against the mandatory algorithm allowlist — a mismatched `sessionTranscript` or `docType` fails the signature. `verifyIssuerSigned` now returns `deviceKey` (the MSO `deviceKeyInfo.deviceKey`) so the two checks chain. The MAC variant (`deviceMac`) is refused with `mdoc/device-mac-unsupported` pending COSE_Mac0 + reader-key support.
|
|
16
|
+
|
|
17
|
+
- v0.12.45 (2026-05-25) — **`b.cose` adds detached-payload sign/verify + `b.cose.importKey` (COSE_Key).** Two RFC 9052 / 9053 completions to the COSE substrate, both useable today and the prerequisites for mdoc device authentication and C2PA claim verification. Detached payloads (RFC 9052 §4.1): b.cose.sign with detached:true emits a COSE_Sign1 whose payload slot is nil — the signature still covers the payload, and the caller transmits it out of band; b.cose.verify takes the payload back as opts.externalPayload and binds it into the Sig_structure. A detached token verified without externalPayload is refused, and supplying externalPayload for an attached token is refused as ambiguous. COSE_Key import (RFC 9052 §7): b.cose.importKey turns a COSE_Key CBOR map into a node:crypto public KeyObject for b.cose.verify, accepting EC2 (P-256 / P-384 / P-521) and OKP (Ed25519) with the curve allowlisted so an unexpected key type is refused. No new runtime dependency. **Added:** *Detached COSE_Sign1 payloads + `b.cose.importKey(coseKey)`* — `b.cose.sign(payload, { detached: true })` emits a nil-payload COSE_Sign1 (the signature covers the payload regardless); `b.cose.verify(coseSign1, { externalPayload })` reconstructs the Sig_structure from the supplied payload, refusing a detached token with no `externalPayload` (`cose/detached-no-payload`) and refusing `externalPayload` on an attached token (`cose/payload-ambiguous`). `b.cose.importKey(coseKey)` maps a COSE_Key map (`kty` 2/EC2 with `crv` P-256/384/521, or `kty` 1/OKP with Ed25519) to a public KeyObject, allowlisting `kty`/`crv` and refusing anything else with `cose/unsupported-key` — the verification key embedded in an mdoc MSO or COSE_Key header is consumed this way.
|
|
18
|
+
|
|
19
|
+
- v0.12.44 (2026-05-25) — **`b.did` adds the did:jwk method.** Completes b.did's method set with did:jwk alongside did:key and did:web. did:jwk encodes a public key as a base64url-encoded JWK directly in the identifier, so resolution is deterministic and offline — the same self-contained shape as did:key but in JWK form, which is what OpenID4VCI and the EU Digital Identity Wallet ecosystem commonly use. b.did.resolve("did:jwk:…") returns the verification key as a node:crypto KeyObject (kty/crv allowlisted — Ed25519 / P-256 / P-384 / secp256k1 — so an unexpected key type is refused, not blindly imported), and b.did.keyToDid(publicKey, { method: "jwk" }) produces a did:jwk from a key (the private member is stripped). No new runtime dependency. **Added:** *did:jwk in `b.did.resolve` / `b.did.keyToDid`* — `resolve` decodes the base64url JWK (bounded via `b.safeJson`), allowlists its `kty`/`crv`, and returns `{ didDocument, verificationMethods: [{ publicKey, … }] }` with the key as a KeyObject ready for `b.vc` / `b.mdoc` / `b.scitt`; `keyToDid(publicKey, { method: "jwk" })` encodes a public key as `did:jwk:<base64url-JWK>` (default remains `did:key`). Malformed base64url-JSON is refused with `did/bad-jwk` and an unsupported key type with `did/unsupported-key`.
|
|
20
|
+
|
|
21
|
+
- v0.12.43 (2026-05-25) — **`b.crypto.selfTest` — FIPS 140-3-style power-on self-test for the crypto stack.** A power-on self-test over the framework's cryptographic primitives — the integrity check a FIPS 140-3-validated module runs at start-up. The hash / XOF checks are known-answer tests against NIST FIPS 202 published vectors (SHA3-256 / SHA3-512 / SHAKE256), so they confirm the framework's hashing matches the standard rather than merely itself; the AEAD check round-trips XChaCha20-Poly1305 and confirms a tampered ciphertext is rejected; and the post-quantum checks run a pairwise-consistency + negative test for ML-KEM-1024, ML-DSA-87, and SLH-DSA-SHAKE-256f (a fresh keypair must encaps/decaps and sign/verify consistently and reject a tampered signature — FIPS 140-3 §10.3 pairwise consistency, since the runtime exposes no seed-injection API for a fixed-seed KAT). selfTest returns a structured report and, by default, throws on any failure so a broken crypto stack fails closed at boot rather than silently producing bad output. Operators in regulated deployments can run it at start-up as a self-integrity gate. **Added:** *`b.crypto.selfTest(opts?)`* — Runs eight checks — SHA3-512 / SHA3-256 / SHAKE256 known-answer tests (NIST FIPS 202), HMAC-SHA3-512 determinism, XChaCha20-Poly1305 round-trip + tamper-detect, and ML-KEM-1024 / ML-DSA-87 / SLH-DSA-SHAKE-256f pairwise-consistency + negative tests — and returns `{ ok, results: [{ name, ok, detail? }], failures, ranAt }`. Throws `crypto/self-test-failed` (with the report attached) on any failure unless `opts.throwOnFailure` is `false`. Exercises the framework's real primitive paths so a self-test failure means the shipped crypto is broken.
|
|
22
|
+
|
|
23
|
+
- v0.12.42 (2026-05-24) — **`b.vc.present` / `b.vc.verifyPresentation` — W3C Verifiable Presentations.** Completes b.vc with the holder side: a Verifiable Presentation is a holder-signed envelope wrapping one or more credentials, proving the presenter controls the key the credentials were issued to. b.vc.present builds and signs a VerifiablePresentation (each credential enveloped per VC-JOSE-COSE) as a compact JWS (vp+jwt) or COSE_Sign1 (application/vp+cose), matching b.vc.issue's algorithms; an optional nonce / audience is embedded in the signed presentation for holder-binding and replay protection. b.vc.verifyPresentation verifies the holder signature (auto-detected jose/cose, mandatory algorithm allowlist, JOSE none refused), the VCDM structure, and the embedded nonce / audience / expectedHolder when given, and — with verifyCredentials: true — verifies each enveloped credential through b.vc.verify and returns them. The holder is typically a DID, resolved to a key via b.did. Composes b.cose; no new runtime dependency. **Added:** *`b.vc.present(opts)` / `b.vc.verifyPresentation(secured, opts)`* — `present` wraps `opts.credentials` (secured VCs — compact-JWS strings or COSE_Sign1 bytes, each enveloped as an `EnvelopedVerifiableCredential` data: URI) in a `VerifiablePresentation` signed by the holder, with optional `nonce` / `audience` embedded for binding. `verifyPresentation` verifies the holder signature against the mandatory `opts.algorithms` allowlist (JOSE `none` always refused), re-checks the VCDM structure, enforces `expectedHolder` / `nonce` / `audience` when supplied, and with `verifyCredentials: true` verifies each enveloped credential through `b.vc.verify` (using `opts.credentialOpts`), returning `{ presentation, holder, credentials, securing, alg }`. The enveloped-credential count is bounded. A `vp+jwt` presentation is refused by `b.vc.verify` and a `vc+jwt` credential is refused by `verifyPresentation` — the media-type binding keeps the two surfaces distinct.
|
|
24
|
+
|
|
11
25
|
- v0.12.41 (2026-05-24) — **`b.did` — W3C DID resolution (did:key + did:web) feeding the credential verifiers.** Resolve W3C Decentralized Identifiers (DID Core 1.0) to verification keys — the link that lets a credential's issuer be named by a DID rather than a raw key. Resolve the issuer DID of a b.vc / b.mdoc / b.scitt credential to a node:crypto KeyObject and hand it to the verifier. did:key encodes the public key in the identifier (multicodec + base58btc), so resolution is deterministic and offline — Ed25519, P-256, P-384, and secp256k1 round-trip; did:web places the DID document at an HTTPS URL derived from the identifier, with the network fetch left to the operator (the framework parses the operator-fetched document and extracts its verification methods, as publicKeyMultibase or publicKeyJwk). b.did.keyToDid encodes a KeyObject as a did:key (an issuer naming itself), b.did.parse splits the identifier (and returns the did:web URL to fetch), and b.did.resolve returns the document and verification keys. DID Core 1.0 is a W3C Recommendation; the method specs (did:key W3C CCG report, did:web DID method registry — EUDI-mandated) are deployed-stable. Composes node:crypto; no new runtime dependency. **Added:** *`b.did.resolve(did, opts?)` / `b.did.keyToDid(publicKey)` / `b.did.parse(did)`* — `resolve` returns `{ didDocument, verificationMethods: [{ id, controller, type, publicKey }] }` with each `publicKey` a `node:crypto` KeyObject ready for `b.vc.verify` / `b.mdoc.verifyIssuerSigned` / `b.scitt.verifyStatement`. did:key resolves deterministically and offline (base58btc + multicodec → Ed25519 raw key or EC compressed point, rebuilt via SPKI); did:web requires the operator to pass the fetched DID document as `opts.document` (the URL to GET is on `b.did.parse(did).url`) and the document `id` must match the requested DID. A publicKeyJwk in a DID document is imported only after its `kty`/`crv` is allowlisted (Ed25519 / P-256 / P-384 / secp256k1) — an unexpected key type from an untrusted document is refused, not blindly imported. `keyToDid` encodes an Ed25519 / P-256 / P-384 / secp256k1 KeyObject as a did:key; `parse` derives the did:web HTTPS URL (`host[:port][:path]` → `https://host/path/did.json`, or `/.well-known/did.json`). Unknown methods, malformed base58, unsupported multicodec codes, and unsupported key types are each refused.
|
|
12
26
|
|
|
13
27
|
- v0.12.40 (2026-05-24) — **`b.mdoc` — ISO 18013-5 mdoc / mDL issuer-data verification.** Verify the issuer-signed data of an ISO/IEC 18013-5 mdoc — the credential format behind mobile driving licences (mDL) and the ISO track of the EU Digital Identity Wallet. This is the relying-party side: confirm that the data elements a holder presents were signed by the issuer and have not been altered. An mdoc's IssuerSigned carries the disclosed data elements and an issuerAuth that is a COSE_Sign1 (b.cose) over a Mobile Security Object (MSO) holding a per-element digest. b.mdoc.verifyIssuerSigned verifies the COSE signature with the issuer certificate from the COSE x5chain header, parses the MSO, enforces its validityInfo window, and recomputes each disclosed element's digest (the full Tag-24 IssuerSignedItemBytes) to match it against the MSO constant-time — the integrity check that makes selective disclosure trustworthy. An absent or mismatched digest is refused. Signing algorithms follow b.cose verification (the classical ES256/384/512 + EdDSA that real mDL issuers use; the caller names the allowlist); opts.trustAnchorsPem additionally verifies the issuer certificate chain. This completes the credential trio alongside W3C VCDM (b.vc) and IETF SD-JWT VC (b.auth.sdJwtVc). Composes b.cose + b.cbor; no new runtime dependency. **Added:** *`b.mdoc.verifyIssuerSigned(issuerSigned, opts)`* — Takes the CBOR `IssuerSigned` map (the operator extracts it from the device response / QR) and returns `{ docType, version, digestAlgorithm, validityInfo, namespaces, signerCert, alg }`. Verifies the COSE_Sign1 `issuerAuth` against the mandatory `opts.algorithms` allowlist using the issuer certificate from its `x5chain` (label 33) header; parses the Tag-24 Mobile Security Object; enforces the MSO `validityInfo` window against `opts.at` (default now; must be a valid Date; malformed dates fail closed); and recomputes the digest of every disclosed `IssuerSignedItem` (over the full Tag-24 bytes, with the MSO `digestAlgorithm` — SHA-256/384/512) to match the MSO `valueDigests` constant-time — an absent or mismatched digest is refused with `mdoc/digest-mismatch`. `opts.expectedDocType` pins the document type; `opts.trustAnchorsPem` (a PEM string or array) additionally verifies the issuer certificate chain and validity at the asserted time. A malformed `x5chain` certificate is refused with a clean `mdoc/bad-cert`. The mdoc device-authentication half (the SessionTranscript-bound holder-binding proof) is a presentation-protocol concern and is not part of issuer-data verification.
|
|
@@ -92,6 +92,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
92
92
|
### Crypto
|
|
93
93
|
|
|
94
94
|
- **At-rest envelope** — envelope-versioned PQC (ML-KEM-1024 + P-384 hybrid, XChaCha20-Poly1305, SHAKE256); vault sealing (`b.crypto`, `b.vault`)
|
|
95
|
+
- **Power-on self-test** — `b.crypto.selfTest()` runs FIPS 140-3-style integrity checks: NIST FIPS 202 known-answer tests (SHA3-256/512, SHAKE256), AEAD round-trip + tamper-detect, and ML-KEM-1024 / ML-DSA-87 / SLH-DSA-SHAKE-256f pairwise-consistency + negative tests; fails closed (throws) on any mismatch
|
|
95
96
|
- **Field-level + crypto-shred** — `b.cryptoField.eraseRow`; per-column data residency tagging + per-row keys (`K_row = HKDF(K_table, rowId)`) so erasing the per-row key makes WAL / replica residuals undecryptable (`b.cryptoField.declareColumnResidency`, `b.cryptoField.declarePerRowKey`)
|
|
96
97
|
- **AAD-bound sealed columns** — AEAD tag tied to `(table, rowId, column, schemaVersion)`; copy-paste between rows or schema-version replay surfaces as refused decrypt (`b.vault.aad`)
|
|
97
98
|
- **Signed webhooks + API encryption** — SLH-DSA-SHAKE-256f default; ML-DSA-65 opt-in; ECIES API encryption (`b.webhook`, `b.crypto`)
|
|
@@ -119,21 +120,21 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
119
120
|
- In-process CIDR fence (`b.middleware.networkAllowlist`)
|
|
120
121
|
- `Cache-Control: no-store` on every 401 from `requireAuth` / `requireAal` / `requireStepUp` per RFC 9111 §5.2.2.5
|
|
121
122
|
- **Outbound HTTP client** — HTTP/1.1 + HTTP/2 with SSRF gate (cloud-metadata IPs hard-denied; private / loopback / link-local overridable per call); scheme + userinfo + per-host destination allowlist; redirects, multipart, interceptors, progress, encrypted cookie jar (`b.httpClient`, `b.ssrfGuard`, `b.safeUrl`)
|
|
122
|
-
- **Network configurability (`b.network`)** — env-driven NTP / NTS (RFC 8915), IPv4/IPv6 NTP, DNS with IPv6 / DoH / DoT (private-CA pinning) / cache / lookup timeout; outbound HTTP proxy (`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`); runtime DPI trust-store CA additions; application-level heartbeats; TCP socket defaults
|
|
123
|
+
- **Network configurability (`b.network`)** — env-driven NTP / NTS (RFC 8915), IPv4/IPv6 NTP, DNS with IPv6 / DoH / DoT (private-CA pinning) / cache / lookup timeout; local DNSSEC signature verification (RFC 4035 — `b.network.dns.dnssec.verifyRrset` over a canonicalised RRset against RSA / ECDSA P-256·P-384 / Ed25519 DNSKEYs, plus DS-digest + key-tag) so a resolver client can verify an answer instead of trusting the upstream AD bit; outbound HTTP proxy (`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`); runtime DPI trust-store CA additions; application-level heartbeats; TCP socket defaults
|
|
123
124
|
- **Error pages** — operator-rendered, no app-frame leakage (`b.errorPage`)
|
|
124
125
|
### Defensive parsers
|
|
125
126
|
|
|
126
127
|
- **JSON / SQL / schema** — `b.safeJson` (with `maxKeys` cap defending CVE-2026-21717 V8 HashDoS), `b.safeBuffer`, `b.safeSql`, `b.safeSchema`
|
|
127
128
|
- **URL + path** — `b.safeUrl` (IDN mixed-script / homograph refuse); `b.safeJsonPath` (refuses filter `?(...)`, deep-scan `$..`, script-shape `(@.x)` for safe Postgres JSONB ops)
|
|
128
129
|
- **Binary codec** — `b.cbor` bounded deterministic CBOR (RFC 8949 §4.2): depth/size caps, indefinite-length + reserved-info + tag + duplicate-key refusal, `requireDeterministic` canonical-form check; the in-tree substrate under COSE / CWT / SCITT / WebAuthn attestation
|
|
129
|
-
- **COSE
|
|
130
|
+
- **COSE messages** — `b.cose` the full RFC 9052 message-type set over `b.cbor`: COSE_Sign1 sign/verify (attached or detached payload), COSE_Encrypt0 single-recipient AEAD, COSE_Mac0 shared-key HMAC (mac0/macVerify0), plus `importKey` (COSE_Key → KeyObject). Signatures use classical ES256/384/512 + EdDSA (final COSE ids, interoperable today) plus ML-DSA-87 (PQC-forward, draft id); bounded + alg-allowlisted + crit-bypass-checked verification; AEAD ChaCha20/Poly1305 default (AES-GCM opt-in); the signed-statement substrate under SCITT / CWT / mdoc / C2PA
|
|
130
131
|
- **CBOR Web Token** — `b.cwt` CWT sign/verify (RFC 8392) over `b.cose`: standard-claim mapping (iss/sub/aud/exp/nbf/iat/cti) + `exp`/`nbf` clock-skew enforcement + `iss`/`aud` matching; the CBOR-native JWT for constrained / IoT / FIDO / verifiable-credential contexts
|
|
131
132
|
- **Entity Attestation Token** — `b.eat` EAT sign/verify (RFC 9711) over `b.cwt`: device + software attestation claims (ueid / oemid / hwmodel / measurements / submods) with verifier-nonce freshness binding, `dbgstat` debug-status policy, and `eat_profile` pinning
|
|
132
133
|
- **SCITT signed statements** — `b.scitt` sign/verify a signed, attributable claim about an artifact (signed SBOM, build attestation, release approval) over `b.cose`: the issuer + subject bind in the integrity-protected CWT_Claims header (RFC 9597); verification refuses any statement missing the iss/sub binding. The issuer side, on finalized RFCs; the transparency receipt (COSE Receipts draft) opts in on publication
|
|
133
134
|
- **Trusted timestamping** — `b.tsa` RFC 3161 timestamp client: `buildRequest` a TimeStampReq, `parseResponse`, and `verifyToken` against your data — the message imprint, sent nonce, critical/sole `id-kp-timeStamping` EKU, and CMS signature are all checked, with optional certificate-chain verification. Timestamp a release artifact, audit checkpoint, or signed statement against any RFC 3161 TSA. Composes `b.cms` + the in-tree ASN.1 DER codec
|
|
134
|
-
- **Verifiable Credentials** — `b.vc` W3C Verifiable Credentials Data Model 2.0 (VC-JOSE-COSE): `issue` / `verify` a signed credential as a compact JWS (`vc+jwt`, ES256/384/512 + EdDSA) or a COSE_Sign1 (`vc+cose`, + ML-DSA-87) over `b.cose`. VCDM structural + `validFrom`/`validUntil` checks; the JOSE `none` algorithm is always refused. The W3C model, distinct from the IETF SD-JWT VC at `b.auth.sdJwtVc`
|
|
135
|
-
- **Mobile credentials (mDL)** — `b.mdoc` ISO/IEC 18013-5
|
|
136
|
-
- **Decentralized Identifiers** — `b.did` W3C DID resolution (DID Core 1.0): `resolve` a `did:key` (deterministic, offline — Ed25519 / P-256 / P-384 / secp256k1) or `did:web` (operator-fetched document) to `node:crypto` verification keys, so a credential's issuer DID resolves to the key that verifies it (`b.vc` / `b.mdoc` / `b.scitt`). `keyToDid` names a key as a `did:key`; document
|
|
135
|
+
- **Verifiable Credentials** — `b.vc` W3C Verifiable Credentials Data Model 2.0 (VC-JOSE-COSE): `issue` / `verify` a signed credential, and `present` / `verifyPresentation` a holder-signed Verifiable Presentation wrapping credentials (with `nonce`/`audience` holder-binding) — as a compact JWS (`vc+jwt` / `vp+jwt`, ES256/384/512 + EdDSA) or a COSE_Sign1 (`vc+cose` / `vp+cose`, + ML-DSA-87) over `b.cose`. VCDM structural + `validFrom`/`validUntil` checks; the JOSE `none` algorithm is always refused. The W3C model, distinct from the IETF SD-JWT VC at `b.auth.sdJwtVc`
|
|
136
|
+
- **Mobile credentials (mDL)** — `b.mdoc` ISO/IEC 18013-5 verification: `verifyIssuerSigned` checks the COSE_Sign1 IssuerAuth (issuer cert from the `x5chain` header), the MSO validity window, and every disclosed element's digest against the MSO `valueDigests` (selective-disclosure integrity), with optional issuer-chain verification; `verifyDeviceAuth` proves holder binding (§9.1.3 signature variant) — the device COSE_Sign1 over the `DeviceAuthentication` structure with the MSO device key + protocol `sessionTranscript`. The ISO credential ecosystem alongside `b.vc` and `b.auth.sdJwtVc`. Composes `b.cose` + `b.cbor`
|
|
137
|
+
- **Decentralized Identifiers** — `b.did` W3C DID resolution (DID Core 1.0): `resolve` a `did:key` / `did:jwk` (deterministic, offline — Ed25519 / P-256 / P-384 / secp256k1) or `did:web` (operator-fetched document) to `node:crypto` verification keys, so a credential's issuer DID resolves to the key that verifies it (`b.vc` / `b.mdoc` / `b.scitt`). `keyToDid` names a key as a `did:key` or `did:jwk`; document/JWK keys are kty/crv-allowlisted before import
|
|
137
138
|
- **Document parsers** — `b.parsers` (XML / TOML / YAML / .env); `b.config` (schema-validated env)
|
|
138
139
|
- **File-type detection** — `b.fileType` magic-byte content classification with deny-on-upload categories (image / document / archive / executable / etc.)
|
|
139
140
|
### Content-safety gates
|
|
@@ -278,6 +278,7 @@ This is the minimum-viable security posture for a production deployment. The fra
|
|
|
278
278
|
- [ ] Confirm `vault: { mode: "wrapped" }` in the app's config (not `"plaintext"`)
|
|
279
279
|
- [ ] Store the passphrase in a secret manager (1Password / Vault / AWS Secrets Manager / sops) — never in git, never in shell history
|
|
280
280
|
- [ ] Rotate the vault passphrase quarterly: `blamejs vault rotate`
|
|
281
|
+
- [ ] In FIPS / regulated deployments, run `b.crypto.selfTest()` at start-up as a power-on integrity gate — it KATs SHA3/SHAKE against NIST FIPS 202 vectors and pairwise-tests ML-KEM-1024 / ML-DSA-87 / SLH-DSA-SHAKE-256f, throwing `crypto/self-test-failed` (fail closed) if the crypto stack is broken
|
|
281
282
|
|
|
282
283
|
**Audit chain**
|
|
283
284
|
- [ ] Run `blamejs audit verify-chain --db <path>` weekly via cron — walks the live audit chain end-to-end and reports tampering with `breakAt` / `breakRowId` / expected-vs-actual prevHash
|
|
@@ -350,6 +351,7 @@ This is the minimum-viable security posture for a production deployment. The fra
|
|
|
350
351
|
- [ ] At boot, before any outbound socket opens: call `b.network.bootFromEnv({ env: process.env, audit: b.audit })` so operator-supplied NTP / DNS / proxy / DPI-trust / TCP socket settings (`BLAMEJS_NTP_*`, `BLAMEJS_DNS_*`, `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`, `BLAMEJS_EXTRA_CA_CERTS`, `BLAMEJS_SOCKET_*`) apply uniformly
|
|
351
352
|
- [ ] If the deployment sits behind a deep-packet-inspection proxy with its own re-signing CA: install the CA via `b.network.tls.addCa("/path/to/corp-ca.pem", { label: "corp-mitm" })` and pass `allowDpiTrust: true` to `b.security.assertProduction` — every CA addition audits with subject + fingerprint so a forensic review can reconstruct the trust path
|
|
352
353
|
- [ ] For authenticated time (HIPAA / PCI / FIPS shops): use `b.network.ntp.nts.query({ host: ntsKeServer })` (RFC 8915) instead of plain SNTP; set `BLAMEJS_NTS_REQUIRE=1` to fail closed on negotiation failure
|
|
354
|
+
- [ ] When a DNS answer drives a trust decision (DANE / TLSA pinning, SSHFP, CAA enforcement, OPENPGPKEY lookup) and the upstream resolver isn't itself trusted: verify the answer's DNSSEC signature with `b.network.dns.dnssec.verifyRrset(...)` rather than trusting the resolver's AD bit — an on-path or compromised resolver can set AD on a forged answer, but cannot forge the RRSIG. Validate the DNSKEY against the parent's DS with `b.network.dns.dnssec.verifyDs(...)` up the chain to a trust anchor you pin
|
|
353
355
|
- [ ] At boot in production: call `await b.security.assertProduction({ vault: "wrapped", dbAtRest: "encrypted", auditSigning: "wrapped", ntpStrict: true, requireEnv: ["BLAMEJS_VAULT_PASSPHRASE"], dataDir: "./data" })` to refuse to start on weak posture instead of warning
|
|
354
356
|
- [ ] At boot: call `await b.configDrift.create({ dataDir, audit }).checkpoint({ allowedOrigins, csp, vaultMode, ... })` so the next boot detects + audits any silent runtime config change
|
|
355
357
|
- [ ] At boot, before any listener opens: call `b.configDrift.verifyVendorIntegrity({ manifestPath: "./lib/vendor/MANIFEST.json", audit: b.audit })` so a tampered `lib/vendor/*.cjs` artifact aborts start instead of running with a swapped crypto bundle
|