@tokenoftrust/storefront-runner 2.4.38 → 2.4.40
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/apps/storefront/env.d.ts +11 -0
- package/apps/storefront/public/shared/commerce-chrome.css +3 -0
- package/apps/storefront/src/components/chrome/SiteFooter.astro +2 -2
- package/apps/storefront/src/lib/rawChrome.ts +21 -3
- package/apps/storefront/src/middleware/renderChain.ts +6 -0
- package/apps/storefront/src/pages/index.astro +2 -2
- package/apps/storefront/src/pages/products/[handle].astro +3 -3
- package/package.json +1 -1
package/apps/storefront/env.d.ts
CHANGED
|
@@ -141,6 +141,17 @@ declare global {
|
|
|
141
141
|
* inside this prefix on the client.
|
|
142
142
|
*/
|
|
143
143
|
basePath: string;
|
|
144
|
+
/**
|
|
145
|
+
* The tenant's REAL routed prefix, always — never overridden to a candidate/
|
|
146
|
+
* revision-pinned render's own prefix the way {@link basePath} is. A pinned
|
|
147
|
+
* preview (`/preview/render/<tenant>/<changeId>`, `/preview/rev/<sha>`,
|
|
148
|
+
* `/preview/staged/<sha>`) has no API mirror under its prefix (the catch-all
|
|
149
|
+
* preview route only renders pages), so any same-origin fetch to a real
|
|
150
|
+
* backend endpoint (`/api/...`) must be built from THIS, not `basePath`, or
|
|
151
|
+
* it 404s under preview. Nav hrefs still want `basePath` so they stay inside
|
|
152
|
+
* the pinned render.
|
|
153
|
+
*/
|
|
154
|
+
apiBasePath: string;
|
|
144
155
|
/**
|
|
145
156
|
* Which customization environment to serve. "live" on production
|
|
146
157
|
* hosts; "test" only behind a valid, tenant-bound preview token on a
|
|
@@ -330,10 +330,13 @@ a.article-card__tag:hover { text-decoration: underline; }
|
|
|
330
330
|
.age-gate__panel { width: min(30rem, 100%); background: var(--color-surface); border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); padding: clamp(24px, 4vw, 40px); text-align: center; }
|
|
331
331
|
.age-gate__mark { font-family: var(--font-display); font-weight: 800; letter-spacing: .04em; font-size: 1.05rem; color: var(--color-text); }
|
|
332
332
|
.age-gate__mark .is-red { color: var(--color-primary); }
|
|
333
|
+
.age-gate__eyebrow { font-family: var(--font-display); font-weight: 700; font-size: .75rem; letter-spacing: .12em; text-transform: uppercase; color: var(--color-muted); }
|
|
333
334
|
.age-gate h2 { margin-top: 14px; font-size: clamp(1.4rem, 3vw, 1.85rem); }
|
|
334
335
|
.age-gate p { margin-top: 10px; color: var(--color-text); }
|
|
335
336
|
.age-gate__actions { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; margin-top: 24px; }
|
|
336
337
|
.age-gate__actions .btn { min-width: 9rem; }
|
|
338
|
+
.age-gate__help { font-size: .875rem; }
|
|
339
|
+
.age-gate__help a { color: var(--color-accent-ink, var(--color-primary)); text-decoration: underline; }
|
|
337
340
|
.age-gate__deny { display: none; }
|
|
338
341
|
.age-gate.is-denied .age-gate__enter, .age-gate.is-denied .age-gate__lede { display: none; }
|
|
339
342
|
.age-gate.is-denied .age-gate__deny { display: block; margin-top: 18px; color: var(--color-error); font-weight: 600; }
|
|
@@ -50,7 +50,7 @@ const {
|
|
|
50
50
|
class: className,
|
|
51
51
|
} = Astro.props;
|
|
52
52
|
|
|
53
|
-
const { basePath } = Astro.locals;
|
|
53
|
+
const { basePath, apiBasePath } = Astro.locals;
|
|
54
54
|
const hrefOf = (l: ChromeLink) => (l.external ? l.href : withBase(basePath, l.href));
|
|
55
55
|
const showLogo = !!brand?.logo && /^(https?:|\/)/.test(brand.logo.src);
|
|
56
56
|
const columnLinkStamp = (l: ChromeLink, colIndex: number, index: number) =>
|
|
@@ -184,7 +184,7 @@ const collapsible = variant === "light-collapsible";
|
|
|
184
184
|
{newsletter.title && <h3 class="font-display text-base font-semibold">{newsletter.title}</h3>}
|
|
185
185
|
{newsletter.body && <p class="mt-1 text-sm text-muted">{newsletter.body}</p>}
|
|
186
186
|
<form
|
|
187
|
-
action={withBase(
|
|
187
|
+
action={withBase(apiBasePath, newsletter.action ?? "/api/newsletter")}
|
|
188
188
|
method="post"
|
|
189
189
|
class="mt-4 flex max-w-md flex-col gap-3 sm:flex-row sm:flex-wrap"
|
|
190
190
|
>
|
|
@@ -47,7 +47,7 @@ import type {
|
|
|
47
47
|
HeaderVariant,
|
|
48
48
|
SocialLink,
|
|
49
49
|
} from "./chrome/model.js";
|
|
50
|
-
import { resolveAgeGateCopy, resolveChromeAssignment } from "./chrome/model.js";
|
|
50
|
+
import { isSafeAgeGateHref, resolveAgeGateCopy, resolveChromeAssignment } from "./chrome/model.js";
|
|
51
51
|
import { resolveHeaderVariant } from "./chrome/variant.js";
|
|
52
52
|
|
|
53
53
|
/** HTML-escape text/attribute content. Covers the five significant characters. */
|
|
@@ -436,6 +436,9 @@ function renderHeader(header: ChromeHeader, variant: HeaderVariant, currentPath?
|
|
|
436
436
|
* can't close the tag early.
|
|
437
437
|
*/
|
|
438
438
|
function renderAgeGate(g: ChromeAgeGate): string {
|
|
439
|
+
// showNo/eyebrow/helpLink defaults mirror resolveAgeGateRuntime (AgeGate.tsx)
|
|
440
|
+
// exactly, so the raw-chrome gate and the component gate present identically
|
|
441
|
+
// for the same chrome.json.
|
|
439
442
|
const resolved = {
|
|
440
443
|
...g,
|
|
441
444
|
title: resolveAgeGateCopy(g.title, g.minAge),
|
|
@@ -444,6 +447,13 @@ function renderAgeGate(g: ChromeAgeGate): string {
|
|
|
444
447
|
noLabel: resolveAgeGateCopy(g.noLabel, g.minAge),
|
|
445
448
|
denyText: resolveAgeGateCopy(g.denyText, g.minAge),
|
|
446
449
|
fineText: resolveAgeGateCopy(g.fineText, g.minAge),
|
|
450
|
+
showNo: g.showNo !== false,
|
|
451
|
+
eyebrow:
|
|
452
|
+
g.eyebrow === false ? null : (resolveAgeGateCopy(g.eyebrow, g.minAge) ?? "Age verification"),
|
|
453
|
+
helpLink:
|
|
454
|
+
g.helpLink && isSafeAgeGateHref(g.helpLink.href)
|
|
455
|
+
? { label: resolveAgeGateCopy(g.helpLink.label, g.minAge) ?? g.helpLink.label, href: g.helpLink.href }
|
|
456
|
+
: null,
|
|
447
457
|
};
|
|
448
458
|
const json = JSON.stringify(resolved).replace(/</g, "\\u003c");
|
|
449
459
|
return `<script type="application/json" id="tot-age-gate-config">${json}</script>`;
|
|
@@ -726,20 +736,28 @@ export const CHROME_BEHAVIOR_JS = `(function () {
|
|
|
726
736
|
return '<span' + (p.className ? ' class="' + gescape(p.className) + '"' : '') + '>' + gescape(p.text) + '</span>';
|
|
727
737
|
}).join('') + '</p>'
|
|
728
738
|
: '';
|
|
739
|
+
var geyebrow = !gmark && gcfg.eyebrow ? '<p class="age-gate__eyebrow">' + gescape(gcfg.eyebrow) + '</p>' : '';
|
|
729
740
|
var gtitle = gescape(gcfg.title || ('Are you ' + gcfg.minAge + ' or older?'));
|
|
730
741
|
var glede = gcfg.lede ? '<p class="age-gate__lede" id="ageGateBody">' + gescape(gcfg.lede) + '</p>' : '';
|
|
731
742
|
var gfine = gcfg.fineText ? '<p class="age-gate__fine">' + gescape(gcfg.fineText) + '</p>' : '';
|
|
732
743
|
var gdeny = gescape(gcfg.denyText || ('You must be ' + gcfg.minAge + ' or older to enter this site.'));
|
|
733
744
|
var gdescribedby = gcfg.lede ? ' aria-describedby="ageGateBody"' : '';
|
|
745
|
+
var gnoButton = gcfg.showNo
|
|
746
|
+
? '<button type="button" class="btn btn-outline age-gate__enter" id="ageNo">' + gescape(gcfg.noLabel || ('No, I am under ' + gcfg.minAge)) + '</button>'
|
|
747
|
+
: '';
|
|
748
|
+
var ghelp = gcfg.helpLink
|
|
749
|
+
? '<p class="age-gate__help"><a href="' + gescape(gcfg.helpLink.href) + '">' + gescape(gcfg.helpLink.label) + '</a></p>'
|
|
750
|
+
: '';
|
|
734
751
|
|
|
735
752
|
mount.innerHTML =
|
|
736
753
|
'<div class="age-gate" id="ageGate" role="dialog" aria-modal="true" aria-labelledby="ageGateTitle"' + gdescribedby + '>' +
|
|
737
|
-
'<div class="age-gate__panel">' + gmark +
|
|
754
|
+
'<div class="age-gate__panel">' + gmark + geyebrow +
|
|
738
755
|
'<h2 id="ageGateTitle">' + gtitle + '</h2>' + glede +
|
|
739
756
|
'<div class="age-gate__actions">' +
|
|
740
757
|
'<button type="button" class="btn btn-primary age-gate__enter" id="ageYes">' + gescape(gcfg.yesLabel || ('Yes, I am ' + gcfg.minAge + '+')) + '</button>' +
|
|
741
|
-
|
|
758
|
+
gnoButton +
|
|
742
759
|
'</div>' +
|
|
760
|
+
ghelp +
|
|
743
761
|
'<p class="age-gate__deny" id="ageDeny">' + gdeny + '</p>' + gfine +
|
|
744
762
|
'</div></div>';
|
|
745
763
|
|
|
@@ -668,6 +668,12 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
668
668
|
// `/preview/render/<tenant>/<changeId>`) so a pinned page's OWN generated
|
|
669
669
|
// links stay inside that candidate instead of escaping to the live host.
|
|
670
670
|
locals.basePath = candidatePin ? candidatePin.basePath : basePath;
|
|
671
|
+
// ALWAYS the real tenant-routed prefix, even under a candidate/revision pin —
|
|
672
|
+
// fb-1791905696516-ubw8a3: a pinned preview's catch-all route only knows how to
|
|
673
|
+
// RENDER a page, so same-origin fetches to a real `/api/...` backend endpoint
|
|
674
|
+
// (checkout handoff, compliance preflight, newsletter) must stay off `basePath`
|
|
675
|
+
// once it's pin-overridden, or they 404 under preview.
|
|
676
|
+
locals.apiBasePath = basePath;
|
|
671
677
|
// Renderer-owned four-stage browser environment (ADR 0014 E1 / 0017), computed
|
|
672
678
|
// ONCE here from authoritative request state and serialized before app code by
|
|
673
679
|
// both document mounts (Layout.astro + rawHtmlResponse). The candidate render
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
type CommerceStaticResolved,
|
|
44
44
|
} from "@/lib/homeVisualParity";
|
|
45
45
|
|
|
46
|
-
const { tenant, tot, content, basePath, canonicalBase, features, capabilities } = Astro.locals;
|
|
46
|
+
const { tenant, tot, content, basePath, apiBasePath, canonicalBase, features, capabilities } = Astro.locals;
|
|
47
47
|
const marketing = !features.catalog;
|
|
48
48
|
|
|
49
49
|
// Raw-HTML home (vendor-injected): serve the tenant's own self-contained
|
|
@@ -296,7 +296,7 @@ const homeSectionWidgets = marketing
|
|
|
296
296
|
</h2>
|
|
297
297
|
{b.body && <p class="mt-3 text-[var(--color-muted)]">{b.body}</p>}
|
|
298
298
|
<form
|
|
299
|
-
action={withBase(
|
|
299
|
+
action={withBase(apiBasePath, "/api/newsletter")}
|
|
300
300
|
method="post"
|
|
301
301
|
class="mx-auto mt-7 flex max-w-md flex-col gap-3 sm:flex-row"
|
|
302
302
|
>
|
|
@@ -49,7 +49,7 @@ import { getRegistryService } from "@/lib/apps/registryAccess.js";
|
|
|
49
49
|
import { renderWidgetSlot } from "@/lib/apps/widgets/renderSlot.js";
|
|
50
50
|
|
|
51
51
|
const { handle } = Astro.params;
|
|
52
|
-
const { tenant, tot, canonicalBase, basePath, commerce } = Astro.locals;
|
|
52
|
+
const { tenant, tot, canonicalBase, basePath, apiBasePath, commerce } = Astro.locals;
|
|
53
53
|
// Commerce-only route: marketing tenants have no product detail pages.
|
|
54
54
|
if (!Astro.locals.features.catalog) return new Response(null, { status: 404 });
|
|
55
55
|
const compliance = tenant.compliance;
|
|
@@ -380,7 +380,7 @@ const jsonLd = [productJsonLd, breadcrumbLd(crumbs, canonicalBase)];
|
|
|
380
380
|
productTitle={product.title}
|
|
381
381
|
checkoutCart={checkoutCart}
|
|
382
382
|
pricing={pricing}
|
|
383
|
-
handoffUrl={withBase(
|
|
383
|
+
handoffUrl={withBase(apiBasePath, "/api/commerce/checkout-handoff")}
|
|
384
384
|
/>
|
|
385
385
|
{/* Regulated-tenant checkout ENFORCEMENT gate. Intercepts the
|
|
386
386
|
managed-cart handoff to clear age + ship-to compliance server-side
|
|
@@ -389,7 +389,7 @@ const jsonLd = [productJsonLd, breadcrumbLd(crumbs, canonicalBase)];
|
|
|
389
389
|
{checkoutCart && isRegulatedTenant(tenant) && compliance?.minAge && (
|
|
390
390
|
<CheckoutComplianceGate
|
|
391
391
|
client:load
|
|
392
|
-
preflightUrl={withBase(
|
|
392
|
+
preflightUrl={withBase(apiBasePath, "/api/compliance/preflight")}
|
|
393
393
|
minAge={compliance.minAge}
|
|
394
394
|
/>
|
|
395
395
|
)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.40",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
|
|
6
6
|
"packageManager": "pnpm@11.9.0",
|