@blamejs/blamejs-shop 0.4.4 → 0.4.5

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 CHANGED
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.4.x
10
10
 
11
+ - v0.4.5 (2026-06-05) — **One sign-in screen: passkey first, email sign-in link as the built-in fallback.** The sign-in page previously offered the passkey ceremony with the email magic-link alternative a page away behind a link. Both now live on one screen: passkey stays the primary action, and an "Email me a sign-in link" form sits inline beneath it — a plain server-rendered form that works with JavaScript disabled. The page meets failure gracefully: a browser without WebAuthn support disables the passkey button and points to the email form, and a failed or cancelled passkey ceremony scrolls the shopper to the fallback with their typed email carried over, one tap from a sign-in link instead of a dead end. Responses are identical whether or not an account exists for the address, the trigger is rate-limited, and a visitor who is already signed in is redirected to their account instead of seeing a login form. **Changed:** *Sign-in page unifies passkey and email-link paths* — Passkey remains the primary action; the email sign-in link form renders inline on the same screen when transactional email is configured, works without JavaScript, and inherits the existing magic-link token semantics unchanged (single-use, expiring). Browsers without WebAuthn support are steered to the email form up front, and any passkey ceremony failure lands the shopper on the fallback with their email pre-filled. The email form's responses do not reveal whether an address has an account, the trigger endpoint is tightly rate-limited, and signed-in visitors are redirected away from the login form.
12
+
11
13
  - v0.4.4 (2026-06-05) — **Placement-targeted promo banners with scheduling, audiences, and click tracking.** Operators can now run placement-specific marketing banners alongside the existing sitewide announcement bar. A banner targets one of six placements — the top strip, the homepage hero, the product-page side, the cart side, the empty-search state, or the footer — with a schedule window, an audience (everyone, signed-in, or guests), one of four visual themes, a priority for choosing among overlapping banners, and a call-to-action whose clicks and impressions are counted. Banners are authored, edited, archived, and restored at /admin/promo-banners, render identically on edge-cached and container-rendered pages, and the click counter works on both. Separately, the edge's Permissions-Policy header now denies the same full directive set as the container's, closing a drift where nine newer denials were missing from edge-served pages, and a parity test keeps the two substrates locked together from here on. The vendored blamejs framework is refreshed from v0.14.21 to v0.14.22. **Added:** *Promo banners* — Define a banner with a slug, placement, headline, optional body, call-to-action, audience, schedule window, theme, and priority; the highest-priority active banner per placement renders. Operator-authored text is escaped at render on both substrates. Clicks route through a counting redirect that works whether the page came from the edge cache or the container; impressions count on container renders. The admin screen covers the full lifecycle including archive and restore, and the same operations are available as JSON under the admin bearer token. The sitewide announcement bar is unchanged and remains the simpler always-on notice strip — if you define a top-strip promo banner while an announcement is active, both render, so pick one for the top of the page. **Fixed:** *Edge pages deny the same Permissions-Policy directives as container pages* — The edge-served pages' Permissions-Policy was missing nine newer directives the container already denied (among them shared-storage, run-ad-auction, join-ad-interest-group, and smartcard). Both substrates now send the identical deny-all list, and a test fails the build if a framework update ever grows one list without the other.
12
14
 
13
15
  - v0.4.3 (2026-06-05) — **Passkey sign-in works again: WebAuthn is permitted on the pages that host it.** The framework's deny-all Permissions-Policy disabled the browser's WebAuthn API everywhere — including the sign-in page's own top-level document — so attempting a passkey sign-in failed with the browser reporting that publickey-credentials-get is not enabled. The policy now permits exactly the WebAuthn capability each ceremony page needs: credential assertion on the sign-in page, credential creation on the registration and passkey-management pages, both scoped to the page's own origin. Every other page keeps the strict deny-all policy, and every other feature (camera, microphone, geolocation, payment outside the payment page) remains denied on the ceremony pages too. **Fixed:** *Passkey ceremonies are no longer blocked by Permissions-Policy* — Sign-in carries publickey-credentials-get=(self); registration and passkey management carry publickey-credentials-create=(self). The allowance is scoped per route following the same pattern the payment page uses, grants apply only to the page's own origin (no cross-origin delegation), and unrecognized feature requests relax nothing. Tests assert the exact header tokens per route and that unrelated pages still deny both WebAuthn features.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.4",
2
+ "version": "0.4.5",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-6k53cvkRrxMgmeStLIoLjVXZQHqIJgTmv1Izd8TYhh1HOC4POgE6GCvx1bsalyEP",
@@ -30,8 +30,8 @@
30
30
  "fingerprinted": "js/passkey-add.b535e6a3eef4514e.js"
31
31
  },
32
32
  "js/passkey-login.js": {
33
- "integrity": "sha384-AbMxT4s0paFnZsEfAHfcpbS2ZRSmZ9KPnttEBy9SvWErBX3U+LhoeYYN7Nm3Y8AX",
34
- "fingerprinted": "js/passkey-login.90c13d7c0d8667e2.js"
33
+ "integrity": "sha384-YcFe/H5GiEIXJ3bvx4RMUKtKwW249rKWyOYbd6xRcDrlAhDeDpt7HGZYPs+R6yWO",
34
+ "fingerprinted": "js/passkey-login.41da9ad7da816e97.js"
35
35
  },
36
36
  "js/passkey-register.js": {
37
37
  "integrity": "sha384-BjuUhbPZ18pHFMyOwT+309BEXu+VAc54RSj8lvN93jfFGDydEgmapiAOKTQM1yma",
package/lib/storefront.js CHANGED
@@ -9129,6 +9129,17 @@ function _setLocaleCookie(res, locale) {
9129
9129
 
9130
9130
  // ---- account-page renderers --------------------------------------------
9131
9131
 
9132
+ // One sign-in screen offering BOTH passwordless paths. The passkey form is
9133
+ // the primary action (a JS island posts the WebAuthn ceremony to
9134
+ // /account/passkey/login-*). The email magic-link is rendered inline as a
9135
+ // SERVER-RENDERED form that POSTs to /account/login/link — it works with
9136
+ // JavaScript disabled and is the always-available backup so a browser
9137
+ // without WebAuthn (or a failed ceremony) is never a dead end. The two
9138
+ // forms are independent <form> elements: the no-JS fallback never depends
9139
+ // on the island. The magic-link block only renders when the operator has
9140
+ // wired the customer-portal primitive AND a transactional mailer (the GET
9141
+ // route sets `magic_link_enabled`); absent it, the page degrades to the
9142
+ // passkey path with no broken affordance.
9132
9143
  var ACCOUNT_LOGIN_PAGE =
9133
9144
  "<section class=\"auth-page\">\n" +
9134
9145
  " <div class=\"auth-card\">\n" +
@@ -9149,6 +9160,23 @@ var ACCOUNT_LOGIN_PAGE =
9149
9160
  " RAW_LOGIN_SCRIPT\n" +
9150
9161
  "</section>\n";
9151
9162
 
9163
+ // The inline email magic-link path on the unified login screen. A distinct
9164
+ // server-rendered <form> (its own email field) that POSTs to
9165
+ // /account/login/link with no JavaScript — the `_injectCsrfFields` wrap
9166
+ // chokepoint stamps the `_csrf` token automatically (the action is not an
9167
+ // EDGE_POST_PATHS prefix), so a no-JS browser submits an accepted token.
9168
+ // `data-passkey-fallback` lets passkey-login.js point a user here in one tap
9169
+ // when WebAuthn is unsupported or the ceremony fails — no dead end.
9170
+ var LOGIN_MAGIC_INLINE =
9171
+ "<div class=\"auth-oauth\" data-passkey-fallback>" +
9172
+ "<div class=\"auth-oauth__divider\"><span>or</span></div>" +
9173
+ "<p class=\"auth-card__lede\">No passkey on this device? We'll email you a single-use sign-in link.</p>" +
9174
+ "<form method=\"post\" action=\"/account/login/link\" class=\"form-stack auth-form auth-form--magic\">" +
9175
+ "<div class=\"form-row\"><label class=\"form-field\"><span class=\"form-field__label\">Email</span><input type=\"email\" name=\"email\" id=\"magic-email\" required autocomplete=\"email\"></label></div>" +
9176
+ "<div class=\"form-actions\"><button type=\"submit\" class=\"btn-secondary auth-form__submit\">Email me a sign-in link</button></div>" +
9177
+ "</form>" +
9178
+ "</div>";
9179
+
9152
9180
  var LOGIN_ERROR_MESSAGES = {
9153
9181
  oauth: "We couldn't complete that sign-in. Please try again.",
9154
9182
  "email-conflict": "That email already has an account — sign in with your passkey instead.",
@@ -9173,9 +9201,9 @@ function renderAccountLogin(opts) {
9173
9201
  var errHtml = (opts.error && LOGIN_ERROR_MESSAGES[opts.error])
9174
9202
  ? "<p class=\"auth-form__message auth-form__message--err\">" + b.template.escapeHtml(LOGIN_ERROR_MESSAGES[opts.error]) + "</p>"
9175
9203
  : "";
9176
- var magicHtml = opts.magic_link_enabled
9177
- ? "<p class=\"auth-card__alt\"><a href=\"/account/login/link\">Email me a sign-in link instead →</a></p>"
9178
- : "";
9204
+ // Render the email-link path INLINE (a working no-JS form), not as a link
9205
+ // to a separate page, so both passwordless paths live on one screen.
9206
+ var magicHtml = opts.magic_link_enabled ? LOGIN_MAGIC_INLINE : "";
9179
9207
  var body = ACCOUNT_LOGIN_PAGE
9180
9208
  .replace("RAW_LOGIN_OAUTH", oauthHtml)
9181
9209
  .replace("RAW_LOGIN_MAGIC", magicHtml)
@@ -13688,6 +13716,19 @@ function mount(router, deps) {
13688
13716
  }
13689
13717
 
13690
13718
  router.get("/account/login", async function (req, res) {
13719
+ // An already-signed-in visitor has no business on the sign-in screen —
13720
+ // send them to their account instead of re-rendering a login form
13721
+ // (mirrors the /account guard's auth read + vault-not-configured catch).
13722
+ var signedIn;
13723
+ try { signedIn = _currentCustomer(req); }
13724
+ catch (e) {
13725
+ if (e && e.code === "vault/not-initialized") return _serviceUnavailable(res, "auth not configured");
13726
+ throw e;
13727
+ }
13728
+ if (signedIn) {
13729
+ res.status(303); res.setHeader && res.setHeader("location", "/account");
13730
+ return res.end ? res.end() : res.send("");
13731
+ }
13691
13732
  var cartCount = await _cartCountForReq(req);
13692
13733
  var url = req.url ? new URL(req.url, "http://localhost") : null;
13693
13734
  // Login captcha is opt-in (CAPTCHA_GATE_LOGIN). The widget + the scoped
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Open-source framework built on blamejs. Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {