@tokenoftrust/storefront-runner 2.0.40 → 2.0.42
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/integrations/lifecycle-host-asset.mjs +20 -6
- package/apps/storefront/package.json +1 -0
- package/apps/storefront/platform-reconcile-tenants.json +3 -0
- package/apps/storefront/public/favicon.ico +0 -0
- package/apps/storefront/public/platform/google-measurement/v2/entry.mjs +1 -1
- package/apps/storefront/public/shared/commerce-chrome.css +147 -16
- package/apps/storefront/src/components/AsyncStylesheet.astro +26 -0
- package/apps/storefront/src/components/Breadcrumbs.astro +8 -1
- package/apps/storefront/src/components/Img.astro +6 -5
- package/apps/storefront/src/components/ProductCard.astro +6 -4
- package/apps/storefront/src/components/Seo.astro +2 -1
- package/apps/storefront/src/components/chrome/SiteFooter.astro +45 -11
- package/apps/storefront/src/components/chrome/SiteHeader.astro +83 -13
- package/apps/storefront/src/components/commerce/RatingStars.astro +1 -1
- package/apps/storefront/src/components/compliance/ComplianceNotice.astro +2 -2
- package/apps/storefront/src/components/content/PromoTiles.astro +107 -0
- package/apps/storefront/src/components/content/TrustStrip.astro +30 -14
- package/apps/storefront/src/components/home/Hero.astro +156 -4
- package/apps/storefront/src/components/islands/AgeGate.tsx +135 -26
- package/apps/storefront/src/components/islands/ImageGallery.tsx +6 -9
- package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +194 -46
- package/apps/storefront/src/components/islands/SearchTypeahead.tsx +10 -4
- package/apps/storefront/src/components/marketing/Faq.astro +9 -2
- package/apps/storefront/src/config/devTenantSeed.ts +1 -1
- package/apps/storefront/src/config/kvTenantResolver.ts +7 -1
- package/apps/storefront/src/config/publicationSpaceConfig.ts +12 -1
- package/apps/storefront/src/config/publicationSpaceRegistry.ts +5 -0
- package/apps/storefront/src/config/tenants.ts +22 -0
- package/apps/storefront/src/layouts/Layout.astro +69 -44
- package/apps/storefront/src/lib/activity/changeActorAttribution.ts +24 -8
- package/apps/storefront/src/lib/activity/recordActivity.ts +15 -0
- package/apps/storefront/src/lib/asyncCss.ts +23 -0
- package/apps/storefront/src/lib/auth/loginGate.ts +22 -11
- package/apps/storefront/src/lib/basePath.ts +70 -9
- package/apps/storefront/src/lib/chrome/managedCart.ts +50 -0
- package/apps/storefront/src/lib/chrome/model.ts +4 -0
- package/apps/storefront/src/lib/chrome/newsletterAgeAttestation.ts +9 -0
- package/apps/storefront/src/lib/content/trustStrip.ts +38 -3
- package/apps/storefront/src/lib/cspStaticHashes.ts +4 -1
- package/apps/storefront/src/lib/diag/capture.ts +161 -0
- package/apps/storefront/src/lib/diag/consoleLine.ts +76 -0
- package/apps/storefront/src/lib/diag/debugConsole.ts +118 -0
- package/apps/storefront/src/lib/diag/debugMode.ts +163 -0
- package/apps/storefront/src/lib/diag/debugWindow.ts +111 -0
- package/apps/storefront/src/lib/diag/reference.ts +148 -0
- package/apps/storefront/src/lib/diag/referenceStore.ts +113 -0
- package/apps/storefront/src/lib/diag/report.ts +109 -0
- package/apps/storefront/src/lib/diag/session.ts +93 -0
- package/apps/storefront/src/lib/featuredProductsPresentation.ts +5 -0
- package/apps/storefront/src/lib/heroSlideshow.ts +41 -0
- package/apps/storefront/src/lib/homeVisualParity.ts +107 -0
- package/apps/storefront/src/lib/imageAttrs.ts +9 -6
- package/apps/storefront/src/lib/integration/nonceStamp.ts +23 -1
- package/apps/storefront/src/lib/integration/renderIntegrationState.ts +36 -0
- package/apps/storefront/src/lib/integration/tenantAnalyticsDeclarations.ts +17 -12
- package/apps/storefront/src/lib/jsonScript.ts +13 -0
- package/apps/storefront/src/lib/pinnedRouteRewriteBoundary.ts +31 -0
- package/apps/storefront/src/lib/previewReload.ts +53 -9
- package/apps/storefront/src/lib/rawChrome.ts +148 -19
- package/apps/storefront/src/lib/rawMarketingHtml.ts +120 -7
- package/apps/storefront/src/lib/storyblok/content-model.ts +46 -3
- package/apps/storefront/src/lib/styleGuideThemes.ts +107 -0
- package/apps/storefront/src/lib/tenantRouting.ts +15 -4
- package/apps/storefront/src/lib/tot/syncState.ts +21 -0
- package/apps/storefront/src/lib/tracking/publishSafetyNet.ts +29 -3
- package/apps/storefront/src/middleware/controlPlane.ts +9 -5
- package/apps/storefront/src/middleware/index.ts +13 -1
- package/apps/storefront/src/middleware/renderChain.ts +54 -11
- package/apps/storefront/src/middleware/routeBranch.ts +58 -12
- package/apps/storefront/src/middleware/staticBundle.ts +37 -3
- package/apps/storefront/src/pages/[...slug].astro +6 -2
- package/apps/storefront/src/pages/api/request-access.ts +5 -4
- package/apps/storefront/src/pages/blog/[slug].astro +11 -1
- package/apps/storefront/src/pages/cart.ts +23 -0
- package/apps/storefront/src/pages/index.astro +64 -85
- package/apps/storefront/src/pages/products/[handle].astro +8 -6
- package/apps/storefront/src/pages/sitemap.xml.ts +8 -2
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +14 -25
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +10 -17
- package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +9 -16
- package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +9 -15
- package/apps/storefront/src/pages/the-build/[slug].astro +7 -1
- package/apps/storefront/src/styles/fonts.css +7 -0
- package/apps/storefront/src/styles/global.css +397 -3
- package/package.json +4 -1
- package/packages/cli/src/visual-parity.mjs +321 -0
- package/packages/public-runtime/package.json +2 -0
- package/packages/public-runtime/schemas/publication-space/v1/manifest.schema.json +1 -1
- package/packages/public-runtime/schemas/publication-space/v1/normalized-mount.schema.json +1 -1
- package/packages/public-runtime/src/chrome.ts +44 -7
- package/packages/public-runtime/src/csp.ts +12 -1
- package/packages/public-runtime/src/extension-contract-values.d.mts +19 -0
- package/packages/public-runtime/src/extension-contract-values.mjs +62 -0
- package/packages/public-runtime/src/extension-contract.ts +20 -6
- package/packages/public-runtime/src/index.ts +8 -0
- package/packages/public-runtime/src/integration/consent-broker.ts +10 -7
- package/packages/public-runtime/src/integration/consent-config.ts +9 -0
- package/packages/public-runtime/src/integration/contact-sales-action-source.ts +56 -10
- package/packages/public-runtime/src/integration/csp-compiler.ts +6 -3
- package/packages/public-runtime/src/integration/governed-event-properties.ts +62 -0
- package/packages/public-runtime/src/integration/index.ts +1 -0
- package/packages/public-runtime/src/integration/lifecycle-boot.ts +34 -6
- package/packages/public-runtime/src/integration/lifecycle-host-asset.d.mts +2 -0
- package/packages/public-runtime/src/integration/lifecycle-host-asset.mjs +17 -3
- package/packages/public-runtime/src/integration/native-amplitude.ts +12 -3
- package/packages/public-runtime/src/integration/native-ga4-sandbox-asset.d.mts +2 -0
- package/packages/public-runtime/src/integration/native-ga4-sandbox-asset.mjs +35 -1
- package/packages/public-runtime/src/integration/native-ga4-sandbox-entry.ts +45 -12
- package/packages/public-runtime/src/integration/native-ga4-sandbox.ts +28 -7
- package/packages/public-runtime/src/integration/native-ga4.ts +18 -2
- package/packages/public-runtime/src/integration/platform-adapters.ts +14 -3
- package/packages/public-runtime/src/integration/render-phases.ts +28 -2
- package/packages/public-runtime/src/publication-redirects.d.mts +45 -0
- package/packages/public-runtime/src/publication-redirects.mjs +379 -0
- package/packages/public-runtime/src/publication-space.ts +24 -3
- package/packages/public-runtime/src/static-bundle.ts +145 -26
- package/packages/public-runtime/src/tenant-embed-defaults.d.mts +3 -0
- package/packages/public-runtime/src/tenant-embed-defaults.mjs +40 -0
- package/packages/public-runtime/src/tenant.ts +59 -1
- package/pnpm-lock.runner.yaml +12 -0
- package/pnpm-workspace.yaml +4 -0
- package/scripts/dev/git-status.mjs +1 -1
- package/scripts/dev/publish.mjs +6 -0
- package/scripts/lib/tenant-asset-copy.mjs +4 -2
- package/scripts/tenant/materialize-tenant-content.mjs +1 -0
- package/scripts/tenant/validate.mjs +171 -8
- package/apps/storefront/src/lib/cfImage.ts +0 -55
- package/apps/storefront/src/workers/pruneRollupWorker.ts +0 -120
- package/tenants/home/public/fonts/inter-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-800-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-800-normal.woff2 +0 -0
- package/tenants/home/public/js/storefront.js +0 -129
- package/tenants/home/public/logo-wordmark.png +0 -0
- package/tenants/home/public/pages/storefront.css +0 -235
- package/tenants/home/theme.json +0 -6
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
PublicationSpaceAliasV1,
|
|
2
3
|
PublicationSpaceManifestV1,
|
|
3
4
|
PublicationSpaceSourceV1,
|
|
4
5
|
} from "@tot/public-runtime";
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* The root publication space for a tenant.
|
|
9
|
+
*
|
|
10
|
+
* `aliases` are the tenant's declared redirects, in the one `redirect-308` contract that carries
|
|
11
|
+
* them the whole way: `publicationRedirects()` lifts them into the aggregate metadata,
|
|
12
|
+
* `cloudfront-live-pointer.mjs` writes them into the live pointer value, and the edge function
|
|
13
|
+
* serves the 308. A tenant declaring none gets a `routes` object without the key at all, so its
|
|
14
|
+
* manifest — and therefore its digest — is unchanged by this parameter existing.
|
|
15
|
+
*/
|
|
6
16
|
export function rootPublicationSpaceConfig(
|
|
7
17
|
tenant: string,
|
|
8
18
|
source: PublicationSpaceSourceV1,
|
|
19
|
+
aliases: readonly PublicationSpaceAliasV1[] = [],
|
|
9
20
|
): PublicationSpaceManifestV1 {
|
|
10
21
|
return {
|
|
11
22
|
version: 1,
|
|
12
23
|
id: "root",
|
|
13
24
|
tenant,
|
|
14
25
|
source,
|
|
15
|
-
routes: { canonicalMount: "/" },
|
|
26
|
+
routes: aliases.length ? { canonicalMount: "/", aliases: [...aliases] } : { canonicalMount: "/" },
|
|
16
27
|
visibility: "public",
|
|
17
28
|
authorization: { policy: "public" },
|
|
18
29
|
discovery: { sitemap: true, feeds: true, publicSearch: true, llms: true },
|
|
@@ -4,6 +4,11 @@ import { rootPublicationSpaceConfig } from "./publicationSpaceConfig.js";
|
|
|
4
4
|
const ROOT_SOURCES: Readonly<Record<string, PublicationSpaceSourceV1>> = {
|
|
5
5
|
home: { forge: "fixture", repo: "home", defaultBranch: "main" },
|
|
6
6
|
"tokenoftrust.com": { forge: "gitea", repo: "tokenoftrust.com-main", defaultBranch: "main" },
|
|
7
|
+
"storefront.tokenoftrust.store": {
|
|
8
|
+
forge: "gitea",
|
|
9
|
+
repo: "storefront.tokenoftrust.store",
|
|
10
|
+
defaultBranch: "main",
|
|
11
|
+
},
|
|
7
12
|
"sample-store.com": { forge: "gitea", repo: "sample-store.com", defaultBranch: "main" },
|
|
8
13
|
"sample-store.e2e.test": { forge: "fixture", repo: "sample-store.e2e.test", defaultBranch: "main" },
|
|
9
14
|
"verify.e2e.test": { forge: "fixture", repo: "verify.e2e.test", defaultBranch: "main" },
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* StaticTenantResolver, tenantResolver) is unchanged — only the data is generic.
|
|
13
13
|
*/
|
|
14
14
|
import type { TenantConfig, TenantResolver } from "@tot/public-runtime";
|
|
15
|
+
import { tenantRendersPublicly } from "@tot/public-runtime";
|
|
15
16
|
import { getTenantTheme } from "@/themes/tenants";
|
|
16
17
|
import { rootPublicationSpaceConfig } from "@/config/publicationSpaceConfig";
|
|
17
18
|
|
|
@@ -96,3 +97,24 @@ export class StaticTenantResolver implements TenantResolver {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
export const tenantResolver: TenantResolver = new StaticTenantResolver();
|
|
100
|
+
|
|
101
|
+
const COMPOSED_PLATFORM_FIELDS = ["access", "pathPrefixTenants"] as const;
|
|
102
|
+
|
|
103
|
+
/** Same contract as the real registry's withPlatformPolicy — kept in sync by hand. */
|
|
104
|
+
export function withPlatformPolicy(record: TenantConfig): TenantConfig {
|
|
105
|
+
const declared = REGISTRY.find((t) => t.tenant_id === record.tenant_id);
|
|
106
|
+
const out = { ...record };
|
|
107
|
+
for (const field of COMPOSED_PLATFORM_FIELDS) {
|
|
108
|
+
const value = declared?.[field];
|
|
109
|
+
if (value === undefined) delete out[field];
|
|
110
|
+
else (out[field] as unknown) = value;
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Same contract as the real registry's tenantIsDeclaredPublic — kept in sync by hand. */
|
|
116
|
+
export function tenantIsDeclaredPublic(tenantId: string): boolean {
|
|
117
|
+
return REGISTRY.some(
|
|
118
|
+
(t) => t.tenant_id === tenantId && tenantRendersPublicly(t),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -23,9 +23,12 @@ import { referenceTheme } from "@/themes/reference";
|
|
|
23
23
|
import { mergeTheme } from "@/themes/schema";
|
|
24
24
|
import { organizationLd, websiteLd } from "@/lib/jsonld";
|
|
25
25
|
import { nonceInlineTags, CHROME_BODY_MARKER } from "@/lib/rawMarketingHtml";
|
|
26
|
+
import { CHROME_BEHAVIOR_JS, sanitizeSharedChromeFragment } from "@/lib/rawChrome";
|
|
27
|
+
import { ASYNC_CSS_SWAP_JS } from "@/lib/asyncCss";
|
|
26
28
|
import { reportTrackingChainHealth } from "@/lib/tracking/interactionHealth";
|
|
27
29
|
import { resolveHeaderVariant, resolveFooterVariant } from "@/lib/chrome/variant";
|
|
28
30
|
import { splitFooterLegal } from "@/lib/chrome/footerLegal";
|
|
31
|
+
import { bindManagedCartAnchors, managedCartHref } from "@/lib/chrome/managedCart";
|
|
29
32
|
import type { HeaderVariant, FooterVariant } from "@/lib/chrome/model";
|
|
30
33
|
import { renderIntegrationPhases, INTERACTION_TRACKING_BOOT_JS } from "@tot/public-runtime";
|
|
31
34
|
import { readViewerSession } from "@/lib/auth/route";
|
|
@@ -127,6 +130,9 @@ const chrome = await Astro.locals.content.getChrome();
|
|
|
127
130
|
// actionable message rather than silently falling back (lib/chrome/variant.ts).
|
|
128
131
|
const resolvedHeaderVariant = resolveHeaderVariant(headerVariant, chrome.header.variant);
|
|
129
132
|
const resolvedFooterVariant = resolveFooterVariant(footerVariant, chrome.footer.variant);
|
|
133
|
+
const footerCompliance = [chrome.footer.warning, chrome.complianceStrip]
|
|
134
|
+
.filter((line): line is string => Boolean(line))
|
|
135
|
+
.join(" ") || undefined;
|
|
130
136
|
|
|
131
137
|
// SHARED CHROME: if the tenant ships content/<id>/chrome.html (the pixel-perfect
|
|
132
138
|
// header/footer the raw marketing pages use), render THAT chrome around the
|
|
@@ -135,7 +141,10 @@ const resolvedFooterVariant = resolveFooterVariant(footerVariant, chrome.footer.
|
|
|
135
141
|
// <!--PAGE_BODY-->: everything before = header, after = footer; pull its tenant
|
|
136
142
|
// stylesheet links into <head>; nonce its inline tags for CSP. Tenants without a
|
|
137
143
|
// chrome.html keep the token-driven Header/Footer (below).
|
|
138
|
-
const chromeHtml =
|
|
144
|
+
const chromeHtml = bindManagedCartAnchors(
|
|
145
|
+
(await Astro.locals.content.getChromeHtml(path)) ?? "",
|
|
146
|
+
Astro.locals.commerce,
|
|
147
|
+
) || null;
|
|
139
148
|
if (chromeHtml) {
|
|
140
149
|
// HEALTH: this is the sharedChrome BYPASS path — a hand-authored chrome.html
|
|
141
150
|
// wins outright over the governed chrome components below, so nothing
|
|
@@ -156,13 +165,22 @@ if (chromeHtml && chromeHtml.includes(CHROME_BODY_MARKER)) {
|
|
|
156
165
|
const marker = chromeHtml.indexOf(CHROME_BODY_MARKER);
|
|
157
166
|
const bodyEnd = chromeHtml.lastIndexOf("</body>");
|
|
158
167
|
sharedChrome = {
|
|
159
|
-
header: nonceInlineTags(
|
|
160
|
-
|
|
168
|
+
header: nonceInlineTags(
|
|
169
|
+
sanitizeSharedChromeFragment(chromeHtml.slice(bodyOpen, marker)),
|
|
170
|
+
cspNonce,
|
|
171
|
+
),
|
|
172
|
+
footer: nonceInlineTags(
|
|
173
|
+
sanitizeSharedChromeFragment(
|
|
174
|
+
chromeHtml.slice(marker + CHROME_BODY_MARKER.length, bodyEnd >= 0 ? bodyEnd : undefined),
|
|
175
|
+
),
|
|
176
|
+
cspNonce,
|
|
177
|
+
),
|
|
161
178
|
css,
|
|
162
179
|
};
|
|
163
180
|
}
|
|
164
181
|
|
|
165
182
|
const compliance = tenant.compliance;
|
|
183
|
+
const cartHref = managedCartHref(Astro.locals.commerce);
|
|
166
184
|
|
|
167
185
|
// Site-chrome data resolution (chrome/* is the canonical, prop-driven
|
|
168
186
|
// component set; this Layout is the ONE place that resolves session/feature/
|
|
@@ -310,7 +328,7 @@ const footerWidgets = await renderWidgetSlot(
|
|
|
310
328
|
<Fragment set:html={sharedChrome.header} />
|
|
311
329
|
) : (
|
|
312
330
|
<>
|
|
313
|
-
<div class="sticky top-0 z-40">
|
|
331
|
+
<div class:list={[resolvedHeaderVariant !== "compact-centered" && "sticky top-0 z-40"]}>
|
|
314
332
|
{chrome.header.announcement?.messages[0] && (
|
|
315
333
|
<AnnouncementBar text={chrome.header.announcement.messages[0]} variant="static" />
|
|
316
334
|
)}
|
|
@@ -322,10 +340,16 @@ const footerWidgets = await renderWidgetSlot(
|
|
|
322
340
|
variant={headerDark ? "dark" : "light"}
|
|
323
341
|
layout={resolvedHeaderVariant}
|
|
324
342
|
announcement={chrome.header.announcement?.messages[0]}
|
|
325
|
-
searchAction={
|
|
343
|
+
searchAction={
|
|
344
|
+
features.productSearch ? (chrome.header.search?.action ?? "/search") : undefined
|
|
345
|
+
}
|
|
346
|
+
searchPlaceholder={chrome.header.search?.placeholder}
|
|
347
|
+
searchLabel={chrome.header.search?.label}
|
|
326
348
|
savedItemsHref={features.savedItems ? "/saved" : undefined}
|
|
327
349
|
headerCtas={chrome.header.ctas}
|
|
328
|
-
|
|
350
|
+
accountHref={chrome.header.accountHref}
|
|
351
|
+
authLink={chrome.header.accountHref ? undefined : authLink}
|
|
352
|
+
cartHref={cartHref}
|
|
329
353
|
sticky={false}
|
|
330
354
|
/>
|
|
331
355
|
</div>
|
|
@@ -339,18 +363,35 @@ const footerWidgets = await renderWidgetSlot(
|
|
|
339
363
|
<ShippingRestrictionNotice text={compliance?.shippingRestriction} />
|
|
340
364
|
|
|
341
365
|
{sharedChrome ? (
|
|
342
|
-
|
|
366
|
+
<>
|
|
367
|
+
<Fragment set:html={sharedChrome.footer} />
|
|
368
|
+
{/* Shared chrome is declarative tenant markup. Its supported behavior is
|
|
369
|
+
supplied from this platform-owned module, never from tenant scripts. */}
|
|
370
|
+
<script is:inline nonce={cspNonce} set:html={CHROME_BEHAVIOR_JS}></script>
|
|
371
|
+
</>
|
|
343
372
|
) : (
|
|
344
373
|
<>
|
|
345
374
|
<SiteFooter
|
|
346
375
|
brand={{
|
|
347
|
-
|
|
348
|
-
|
|
376
|
+
logo: chrome.footer.brand?.logo,
|
|
377
|
+
wordmark:
|
|
378
|
+
chrome.footer.brand?.wordmark?.map((part) => part.text).join("") ??
|
|
379
|
+
tenant.displayName,
|
|
380
|
+
contact: chrome.footer.brand?.text?.split(/\r?\n/),
|
|
349
381
|
}}
|
|
350
382
|
variant={resolvedFooterVariant}
|
|
351
383
|
columns={footerColumns}
|
|
352
|
-
newsletter={
|
|
384
|
+
newsletter={
|
|
385
|
+
features.newsletter
|
|
386
|
+
? (chrome.footer.newsletter ?? {
|
|
387
|
+
title: "Newsletter",
|
|
388
|
+
placeholder: "you@example.com",
|
|
389
|
+
})
|
|
390
|
+
: undefined
|
|
391
|
+
}
|
|
392
|
+
payments={chrome.footer.payments}
|
|
353
393
|
social={chrome.footer.social}
|
|
394
|
+
compliance={footerCompliance}
|
|
354
395
|
legal={legalLine}
|
|
355
396
|
bottomLinks={bottomLinks}
|
|
356
397
|
/>
|
|
@@ -372,7 +413,13 @@ const footerWidgets = await renderWidgetSlot(
|
|
|
372
413
|
))}
|
|
373
414
|
|
|
374
415
|
{capabilities.ageVerification.enabled && compliance?.minAge && (
|
|
375
|
-
<IsolatedAgeGate
|
|
416
|
+
<IsolatedAgeGate
|
|
417
|
+
client:load
|
|
418
|
+
minAge={compliance.minAge}
|
|
419
|
+
brand={tenant.displayName}
|
|
420
|
+
tenantId={tenant.tenant_id}
|
|
421
|
+
presentation={chrome.ageGate}
|
|
422
|
+
/>
|
|
376
423
|
)}
|
|
377
424
|
|
|
378
425
|
{/* Global quick-view dialog (opened by any [data-quickview] card button).
|
|
@@ -451,39 +498,10 @@ const footerWidgets = await renderWidgetSlot(
|
|
|
451
498
|
/<domain>/ path segment (locals.basePath), keep same-origin
|
|
452
499
|
root-absolute links inside that prefix so navigation stays on the
|
|
453
500
|
tenant. Host-routed requests have no base path and skip this entirely. */}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
if (!base) return;
|
|
459
|
-
function fix(a) {
|
|
460
|
-
var href = a.getAttribute("href");
|
|
461
|
-
if (!href || href.charAt(0) !== "/" || href.charAt(1) === "/") return;
|
|
462
|
-
// Global asset namespaces are root-served under every tenant.
|
|
463
|
-
if (href.indexOf("/tenants/") === 0 || href.indexOf("/shared/") === 0) return;
|
|
464
|
-
if (href === base || href.indexOf(base + "/") === 0) return;
|
|
465
|
-
a.setAttribute("href", base + href);
|
|
466
|
-
}
|
|
467
|
-
function fixAll() {
|
|
468
|
-
document.querySelectorAll('a[href^="/"]').forEach(fix);
|
|
469
|
-
}
|
|
470
|
-
if (document.readyState === "loading") {
|
|
471
|
-
document.addEventListener("DOMContentLoaded", fixAll);
|
|
472
|
-
} else {
|
|
473
|
-
fixAll();
|
|
474
|
-
}
|
|
475
|
-
// Late/island-rendered anchors: fix just-in-time before navigation.
|
|
476
|
-
document.addEventListener(
|
|
477
|
-
"pointerdown",
|
|
478
|
-
function (e) {
|
|
479
|
-
var a = e.target && e.target.closest && e.target.closest('a[href^="/"]');
|
|
480
|
-
if (a) fix(a);
|
|
481
|
-
},
|
|
482
|
-
true,
|
|
483
|
-
);
|
|
484
|
-
})();
|
|
485
|
-
</script>
|
|
486
|
-
)}
|
|
501
|
+
<script>
|
|
502
|
+
import { bindBasePathAnchors } from "@/lib/basePath";
|
|
503
|
+
bindBasePathAnchors();
|
|
504
|
+
</script>
|
|
487
505
|
|
|
488
506
|
{/* Dev-loop theme hot-patch: tenant-hot-reload.mjs broadcasts a
|
|
489
507
|
`tot:theme-vars` HMR custom event on every theme.json save instead of
|
|
@@ -573,6 +591,13 @@ const footerWidgets = await renderWidgetSlot(
|
|
|
573
591
|
entry in lib/cspStaticHashes.ts. */}
|
|
574
592
|
<script is:inline nonce={cspNonce} set:html={INTERACTION_TRACKING_BOOT_JS}></script>
|
|
575
593
|
|
|
594
|
+
{/* Swaps every <AsyncStylesheet>-rendered preload link to rel="stylesheet"
|
|
595
|
+
once fetched. ONE nonced script per document handles every such link,
|
|
596
|
+
however many <AsyncStylesheet> instances rendered (lib/asyncCss.ts —
|
|
597
|
+
the onload="" attribute trick can't carry a nonce under this CSP). A
|
|
598
|
+
no-op when the page has none. */}
|
|
599
|
+
<script is:inline nonce={cspNonce} set:html={ASYNC_CSS_SWAP_JS}></script>
|
|
600
|
+
|
|
576
601
|
{/* document.deferred global-entry boundary (ext-kernel-04) — the last phase, the
|
|
577
602
|
renderer-owned mount point for the deferred integration entry. This region also
|
|
578
603
|
CARRIES the lifecycle host tag (ext-kernel-05), which initializes the plan's
|
|
@@ -60,22 +60,38 @@ function submitAuthorizationKey(
|
|
|
60
60
|
* Persist who took `action` on `subjectId` (a `changeId` for reject/accept/build,
|
|
61
61
|
* or the aggregate id for ship). Best-effort — never blocks or fails the
|
|
62
62
|
* mutation it describes; a write fault is swallowed, mirroring `recordActivity`.
|
|
63
|
+
*
|
|
64
|
+
* `email: null` means the request carried no real per-person identity — the
|
|
65
|
+
* headless Bearer/CLI operator path, which authenticates a shared tenant-level
|
|
66
|
+
* secret, never a signed-in developer (the CLI holds no storefront viewer
|
|
67
|
+
* session). Recording that case under the tenant OWNER's email would credit
|
|
68
|
+
* the owner's account for an action any operator-secret holder could have
|
|
69
|
+
* taken — a real misattribution, not a display nicety — so it is recorded as
|
|
70
|
+
* a distinct `service` identity instead.
|
|
63
71
|
*/
|
|
64
72
|
export async function recordChangeActor(
|
|
65
73
|
kv: ChangeActorKv,
|
|
66
74
|
tenantId: string,
|
|
67
75
|
subjectId: string,
|
|
68
76
|
action: ChangeActorAction,
|
|
69
|
-
email: string,
|
|
77
|
+
email: string | null,
|
|
70
78
|
): Promise<void> {
|
|
71
79
|
try {
|
|
72
|
-
const record: ChangeActorRecord =
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
const record: ChangeActorRecord = email
|
|
81
|
+
? {
|
|
82
|
+
kind: "human",
|
|
83
|
+
subject: `email:${email.trim().toLowerCase()}`,
|
|
84
|
+
email,
|
|
85
|
+
name: displayNameFromEmail(email),
|
|
86
|
+
at: new Date().toISOString(),
|
|
87
|
+
}
|
|
88
|
+
: {
|
|
89
|
+
kind: "service",
|
|
90
|
+
subject: "service:cli-operator",
|
|
91
|
+
email: null,
|
|
92
|
+
name: "CLI (operator credential)",
|
|
93
|
+
at: new Date().toISOString(),
|
|
94
|
+
};
|
|
79
95
|
await kv.put(attributionKey(tenantId, subjectId, action), JSON.stringify(record));
|
|
80
96
|
} catch {
|
|
81
97
|
/* best-effort — the mutation it describes already succeeded */
|
|
@@ -87,6 +87,21 @@ export function actorEmailFor(context: APIContext, auth: { owner: string }): str
|
|
|
87
87
|
return context.locals?.viewer?.email ?? auth.owner;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* The real per-person identity to CREDIT for an owner-facing "who did this"
|
|
92
|
+
* record — DELIBERATELY narrower than {@link actorEmailFor}. That helper's
|
|
93
|
+
* owner fallback exists only to give the anonymized activity hash and
|
|
94
|
+
* authorization checks a valid bucket/identity to reason about; crediting the
|
|
95
|
+
* SAME fallback to `recordChangeActor` would misattribute every action taken
|
|
96
|
+
* over the headless Bearer/CLI operator path (a shared tenant-level secret,
|
|
97
|
+
* never a signed-in developer — the CLI holds no storefront viewer session)
|
|
98
|
+
* to the tenant owner personally. `null` means exactly that: no real person
|
|
99
|
+
* to credit, not "credit the owner." See `changeActorAttribution.ts`.
|
|
100
|
+
*/
|
|
101
|
+
export function attributedActorEmailFor(context: APIContext): string | null {
|
|
102
|
+
return context.locals?.viewer?.email ?? null;
|
|
103
|
+
}
|
|
104
|
+
|
|
90
105
|
/**
|
|
91
106
|
* Best-effort, deterministic display name from an email — a real name is NOT
|
|
92
107
|
* captured anywhere in the current session/grant model. A placeholder good enough
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSP-safe async (non-render-blocking) CSS loading.
|
|
3
|
+
*
|
|
4
|
+
* The classic `<link rel="preload" as="style" onload="this.rel='stylesheet'">`
|
|
5
|
+
* trick doesn't work under this storefront's CSP (docs/architecture/async-css.md):
|
|
6
|
+
* `onload="…"` is an inline EVENT-HANDLER ATTRIBUTE, governed by `script-src`
|
|
7
|
+
* (there is no separate `script-src-attr` directive here), and an attribute
|
|
8
|
+
* can't carry a per-request nonce — only a `<script nonce>` TAG can. So the
|
|
9
|
+
* swap has to happen from a nonced `<script>`, not an attribute.
|
|
10
|
+
*
|
|
11
|
+
* Rather than emit one nonced script per stylesheet, `<AsyncStylesheet>`
|
|
12
|
+
* (../components/AsyncStylesheet.astro) tags its `<link rel="preload">` with
|
|
13
|
+
* `data-async-css`, and Layout.astro renders `ASYNC_CSS_SWAP_JS` ONCE per page
|
|
14
|
+
* — it flips every matching link's `rel` to `stylesheet` no matter how many
|
|
15
|
+
* `<AsyncStylesheet>` instances rendered. A `<noscript>` fallback covers
|
|
16
|
+
* no-JS. fb-1789228922973-oidvf5.
|
|
17
|
+
*/
|
|
18
|
+
export const ASYNC_CSS_ATTR = "data-async-css";
|
|
19
|
+
|
|
20
|
+
export const ASYNC_CSS_SWAP_JS = `(function () {
|
|
21
|
+
var links = document.querySelectorAll('link[rel="preload"][as="style"][${ASYNC_CSS_ATTR}]');
|
|
22
|
+
for (var i = 0; i < links.length; i++) links[i].rel = 'stylesheet';
|
|
23
|
+
})();`;
|
|
@@ -19,14 +19,6 @@ import type { SessionRecord } from "./session.js";
|
|
|
19
19
|
|
|
20
20
|
export type GateMode = "password" | "login";
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
* The platform's own "self" home tenant (tenants.ts `HOME_TENANT`, registry
|
|
24
|
-
* default). It is NEVER login-gated — `storefront.tokenoftrust.store` renders its
|
|
25
|
-
* public home so people can browse + sign up. Every OTHER tenant on a gated host
|
|
26
|
-
* (path-prefix or per-tenant subdomain) is gated behind sign-in.
|
|
27
|
-
*/
|
|
28
|
-
export const PUBLIC_HOME_TENANT_ID = "home";
|
|
29
|
-
|
|
30
22
|
/**
|
|
31
23
|
* The record fields the host-admission + host-capability checks read. All optional
|
|
32
24
|
* — these readers tolerate a partial record and are deny-closed on absence.
|
|
@@ -139,9 +131,27 @@ export function isAuthPath(pathname: string): boolean {
|
|
|
139
131
|
* unauthenticated `tot` install (src/pages/api/cli/policy.ts). Kept exact (not a
|
|
140
132
|
* broad `/api/cli/` prefix) so a future authed CLI endpoint isn't accidentally
|
|
141
133
|
* exposed.
|
|
134
|
+
*
|
|
135
|
+
* `/api/preview/version` is the shared preview's on-publish refresh SIGNAL
|
|
136
|
+
* (src/pages/api/preview/version.ts): a read-only per-tenant edge cache-version
|
|
137
|
+
* integer, no-store and noindex, carrying nothing a viewer of the preview cannot
|
|
138
|
+
* already see. It must be exempt HERE rather than by relaxing
|
|
139
|
+
* {@link isBypassPrivilegedPath}, which excludes every `/api/` route from the
|
|
140
|
+
* preview-token and QA bypasses on purpose — that exclusion is the security
|
|
141
|
+
* boundary and stays intact.
|
|
142
|
+
*
|
|
143
|
+
* Matched in BOTH addressing forms, because the injected reload client fetches it
|
|
144
|
+
* under the page's basePath: a path-routed preview sends `/<tenant>/api/preview/
|
|
145
|
+
* version`, so a bare-only match leaves the gate answering the "Coming soon"
|
|
146
|
+
* holding page with a 200. The client reads `(await res.json()).version` inside a
|
|
147
|
+
* catch that treats every failure as transient, so an HTML body there is swallowed
|
|
148
|
+
* silently and the reload can never fire while the tab polls once a second forever.
|
|
142
149
|
*/
|
|
143
150
|
export function isPublicPath(pathname: string): boolean {
|
|
144
|
-
|
|
151
|
+
if (pathname === "/api/cli/policy") return true;
|
|
152
|
+
if (pathname === "/api/preview/version") return true;
|
|
153
|
+
if (/^\/[^/]+\/api\/preview\/version$/.test(pathname)) return true;
|
|
154
|
+
return false;
|
|
145
155
|
}
|
|
146
156
|
|
|
147
157
|
/**
|
|
@@ -277,8 +287,9 @@ export interface LoginGateInput {
|
|
|
277
287
|
/** True when a valid, resource-bound session was resolved for this request. */
|
|
278
288
|
hasValidSession: boolean;
|
|
279
289
|
/**
|
|
280
|
-
* True when the resolved tenant
|
|
281
|
-
*
|
|
290
|
+
* True when the resolved tenant declares itself publicly accessible, exempting
|
|
291
|
+
* it from the gate so it renders to anyone. Sourced from the tenant's own
|
|
292
|
+
* `access.public` declaration via `tenantRendersPublicly` — never a tenant id.
|
|
282
293
|
*/
|
|
283
294
|
tenantExempt?: boolean;
|
|
284
295
|
/**
|
|
@@ -13,20 +13,53 @@
|
|
|
13
13
|
*
|
|
14
14
|
* It is idempotent and conservative: only root-absolute single-slash paths are
|
|
15
15
|
* prefixed. Protocol-relative URLs ("//cdn…"), absolute URLs ("https://…"),
|
|
16
|
-
* fragments/queries ("#x", "?x"),
|
|
17
|
-
*
|
|
18
|
-
* under both routing modes and must NOT carry a tenant prefix:
|
|
19
|
-
* - `/tenants/…` — per-tenant static assets (logo, favicon, theme/mkt css);
|
|
20
|
-
* - `/shared/…` — platform-shipped shared assets (shared commerce-chrome css).
|
|
21
|
-
* This mirrors both `prefixTenantLinks` (lib/rawMarketingHtml.ts) and the
|
|
22
|
-
* defense-in-depth client script in Layout.astro.
|
|
16
|
+
* fragments/queries ("#x", "?x"), already-prefixed paths, and every
|
|
17
|
+
* {@link ROOT_SERVED_HREF_NAMESPACES} path pass through.
|
|
23
18
|
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Root-served namespaces that a tenant prefix must never be applied to.
|
|
22
|
+
*
|
|
23
|
+
* These are served at the ORIGIN ROOT under both routing modes: host-routed, the
|
|
24
|
+
* platform's static handler answers them before the Worker; path-routed, the same
|
|
25
|
+
* bytes still live at the root. A prefixed `/<tenant>/favicon.svg` is not a static
|
|
26
|
+
* asset, so it costs a redirect at best.
|
|
27
|
+
*
|
|
28
|
+
* `tenants` is per-tenant static assets (already tenant-scoped in the path, so a
|
|
29
|
+
* second prefix would double it). The rest mirror `PLATFORM_STATIC_ASSET_PREFIXES`
|
|
30
|
+
* in `src/middleware/routeBranch.ts` — the serving-side authority for the same set.
|
|
31
|
+
* They are restated here rather than imported because this module ships to the
|
|
32
|
+
* browser and in the published runner tree, which is assembled from a COPY LIST
|
|
33
|
+
* rather than a dependency graph. `rawMarketingHtml.test.ts` asserts this list
|
|
34
|
+
* covers that one, so the two cannot drift apart unnoticed.
|
|
35
|
+
*
|
|
36
|
+
* Consumers: {@link withBase} (and through it {@link bindBasePathAnchors}) and
|
|
37
|
+
* `prefixTenantLinks` (lib/rawMarketingHtml.ts).
|
|
38
|
+
*/
|
|
39
|
+
export const ROOT_SERVED_HREF_NAMESPACES = [
|
|
40
|
+
"_a",
|
|
41
|
+
"_astro",
|
|
42
|
+
"apps",
|
|
43
|
+
"assets",
|
|
44
|
+
"brand",
|
|
45
|
+
"favicon.svg",
|
|
46
|
+
"js",
|
|
47
|
+
"platform",
|
|
48
|
+
"shared",
|
|
49
|
+
"tenants",
|
|
50
|
+
] as const;
|
|
51
|
+
|
|
52
|
+
/** True when root-absolute `href` names a {@link ROOT_SERVED_HREF_NAMESPACES} path. */
|
|
53
|
+
export function isRootServedHref(href: string): boolean {
|
|
54
|
+
const path = href.slice(1).split(/[?#]/, 1)[0] ?? "";
|
|
55
|
+
return ROOT_SERVED_HREF_NAMESPACES.some((ns) => path === ns || path.startsWith(`${ns}/`));
|
|
56
|
+
}
|
|
57
|
+
|
|
24
58
|
export function withBase(basePath: string, href: string): string {
|
|
25
59
|
if (!basePath || !href) return href;
|
|
26
60
|
// Only same-origin root-absolute paths; skip protocol-relative "//".
|
|
27
61
|
if (href.charAt(0) !== "/" || href.charAt(1) === "/") return href;
|
|
28
|
-
|
|
29
|
-
if (href.startsWith("/tenants/") || href.startsWith("/shared/")) return href;
|
|
62
|
+
if (isRootServedHref(href)) return href;
|
|
30
63
|
// Already inside the prefix.
|
|
31
64
|
if (href === basePath || href.startsWith(`${basePath}/`)) return href;
|
|
32
65
|
return `${basePath}${href}`;
|
|
@@ -46,3 +79,31 @@ export function clientBasePath(): string {
|
|
|
46
79
|
export function withClientBase(href: string): string {
|
|
47
80
|
return withBase(clientBasePath(), href);
|
|
48
81
|
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Keep same-origin root-absolute anchors inside the tenant prefix, so navigation on a
|
|
85
|
+
* path-routed page stays on the tenant. Covers anchors in raw tenant HTML and chrome,
|
|
86
|
+
* which never pass through {@link withBase}. A no-op when host-routed.
|
|
87
|
+
*/
|
|
88
|
+
export function bindBasePathAnchors(doc: Document = document): void {
|
|
89
|
+
const base = doc.documentElement.dataset.basePath ?? "";
|
|
90
|
+
if (!base) return;
|
|
91
|
+
const fix = (a: Element) => {
|
|
92
|
+
const href = a.getAttribute("href");
|
|
93
|
+
if (!href) return;
|
|
94
|
+
const next = withBase(base, href);
|
|
95
|
+
if (next !== href) a.setAttribute("href", next);
|
|
96
|
+
};
|
|
97
|
+
const fixAll = () => doc.querySelectorAll('a[href^="/"]').forEach(fix);
|
|
98
|
+
if (doc.readyState === "loading") doc.addEventListener("DOMContentLoaded", fixAll);
|
|
99
|
+
else fixAll();
|
|
100
|
+
// Late/island-rendered anchors: fix just-in-time before navigation.
|
|
101
|
+
doc.addEventListener(
|
|
102
|
+
"pointerdown",
|
|
103
|
+
(e) => {
|
|
104
|
+
const a = e.target instanceof Element ? e.target.closest('a[href^="/"]') : null;
|
|
105
|
+
if (a) fix(a);
|
|
106
|
+
},
|
|
107
|
+
true,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ResolvedManagedCheckout } from "@tot/public-runtime";
|
|
2
|
+
|
|
3
|
+
/** The managed checkout is the sole cart destination when it is available. */
|
|
4
|
+
export function managedCartHref(
|
|
5
|
+
commerce: ResolvedManagedCheckout | null | undefined,
|
|
6
|
+
): string | undefined {
|
|
7
|
+
return commerce?.cartUrl;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ANCHOR_RE = /<a\b[^>]*>[\s\S]*?<\/a>/gi;
|
|
11
|
+
|
|
12
|
+
function isCartAnchor(anchor: string): boolean {
|
|
13
|
+
const openingTag = anchor.slice(0, anchor.indexOf(">") + 1);
|
|
14
|
+
const classValue = openingTag.match(/\bclass=(?:"([^"]*)"|'([^']*)')/i);
|
|
15
|
+
const classes = (classValue?.[1] ?? classValue?.[2] ?? "").split(/\s+/);
|
|
16
|
+
const ariaLabel = openingTag.match(/\baria-label=(?:"([^"]*)"|'([^']*)')/i);
|
|
17
|
+
const label = ariaLabel?.[1] ?? ariaLabel?.[2] ?? "";
|
|
18
|
+
return classes.includes("cart") || /^Cart(?:\b|,)/i.test(label);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function escapeAttribute(value: string): string {
|
|
22
|
+
return value
|
|
23
|
+
.replaceAll("&", "&")
|
|
24
|
+
.replaceAll('"', """)
|
|
25
|
+
.replaceAll("<", "<")
|
|
26
|
+
.replaceAll(">", ">");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Bind raw/shared chrome cart affordances to the same request-scoped checkout
|
|
31
|
+
* destination used by product forms. If checkout is unavailable, remove the
|
|
32
|
+
* affordance instead of sending shoppers to the storefront's nonexistent
|
|
33
|
+
* `/cart` page. Works on rendered HTML, so generated and tenant-authored shared
|
|
34
|
+
* chrome follow the same rule without tenant-specific branches.
|
|
35
|
+
*/
|
|
36
|
+
export function bindManagedCartAnchors(
|
|
37
|
+
html: string,
|
|
38
|
+
commerce: ResolvedManagedCheckout | null | undefined,
|
|
39
|
+
): string {
|
|
40
|
+
const href = managedCartHref(commerce);
|
|
41
|
+
return html.replace(ANCHOR_RE, (anchor) => {
|
|
42
|
+
if (!isCartAnchor(anchor)) return anchor;
|
|
43
|
+
if (!href) return "";
|
|
44
|
+
const escaped = escapeAttribute(href);
|
|
45
|
+
if (/\bhref=(?:"[^"]*"|'[^']*')/i.test(anchor)) {
|
|
46
|
+
return anchor.replace(/\bhref=(?:"[^"]*"|'[^']*')/i, `href="${escaped}"`);
|
|
47
|
+
}
|
|
48
|
+
return anchor.replace(/^<a\b/i, `<a href="${escaped}"`);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -26,6 +26,10 @@ export {
|
|
|
26
26
|
type ChromeUtilityNav,
|
|
27
27
|
type ChromeAnnouncement,
|
|
28
28
|
type ChromeAgeGate,
|
|
29
|
+
CHROME_AGE_GATE_VARIANTS,
|
|
30
|
+
type ChromeAgeGateVariant,
|
|
31
|
+
deriveAgeGateStorageKey,
|
|
32
|
+
resolveAgeGateCopy,
|
|
29
33
|
type ChromeFooterBrand,
|
|
30
34
|
type ChromeTrustMark,
|
|
31
35
|
type ChromeHead,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const NEWSLETTER_AGE_ATTESTATION = Object.freeze({
|
|
2
|
+
name: "age_attestation",
|
|
3
|
+
value: "21plus",
|
|
4
|
+
label: "I confirm I am 21 years of age or older and agree to receive marketing emails.",
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
export function resolveNewsletterAgeAttestation(enabled: boolean | undefined) {
|
|
8
|
+
return enabled ? NEWSLETTER_AGE_ATTESTATION : undefined;
|
|
9
|
+
}
|