@tokenoftrust/storefront-runner 0.1.0
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/LICENSE +58 -0
- package/README.md +40 -0
- package/apps/storefront/.dev.vars.example +41 -0
- package/apps/storefront/.env.example +24 -0
- package/apps/storefront/astro.config.mjs +245 -0
- package/apps/storefront/env.d.ts +155 -0
- package/apps/storefront/lib/ensure-preview-reconcile-secret.sh +31 -0
- package/apps/storefront/lib/resolve-cf-token.sh +48 -0
- package/apps/storefront/lib/resolve-tot-credentials.sh +69 -0
- package/apps/storefront/migrations/0000_baseline.sql +54 -0
- package/apps/storefront/migrations/0001_products_fts.sql +9 -0
- package/apps/storefront/migrations/README.md +54 -0
- package/apps/storefront/migrations/meta/0000_snapshot.json +347 -0
- package/apps/storefront/migrations/meta/0001_snapshot.json +347 -0
- package/apps/storefront/migrations/meta/_journal.json +20 -0
- package/apps/storefront/package.json +41 -0
- package/apps/storefront/public/brand/northwind-logo-dark.svg +4 -0
- package/apps/storefront/public/brand/northwind-logo-light.svg +4 -0
- package/apps/storefront/public/brand/northwind-og.svg +13 -0
- package/apps/storefront/public/favicon.svg +5 -0
- package/apps/storefront/public/js/dashboard-team.js +147 -0
- package/apps/storefront/public/js/dashboard-vendor-search.js +115 -0
- package/apps/storefront/src/components/Badge.astro +34 -0
- package/apps/storefront/src/components/Breadcrumbs.astro +45 -0
- package/apps/storefront/src/components/CollectionCard.astro +56 -0
- package/apps/storefront/src/components/EmptyState.astro +35 -0
- package/apps/storefront/src/components/Img.astro +68 -0
- package/apps/storefront/src/components/ProductCard.astro +228 -0
- package/apps/storefront/src/components/Section.astro +39 -0
- package/apps/storefront/src/components/Seo.astro +70 -0
- package/apps/storefront/src/components/WidgetFrame.astro +34 -0
- package/apps/storefront/src/components/auth/AuthBadge.astro +44 -0
- package/apps/storefront/src/components/chrome/AnnouncementBar.astro +108 -0
- package/apps/storefront/src/components/chrome/MegaMenu.astro +26 -0
- package/apps/storefront/src/components/chrome/NavDropdown.astro +116 -0
- package/apps/storefront/src/components/chrome/SiteFooter.astro +181 -0
- package/apps/storefront/src/components/chrome/SiteHeader.astro +181 -0
- package/apps/storefront/src/components/chrome/UtilityNav.astro +79 -0
- package/apps/storefront/src/components/commerce/FoxyLoader.astro +23 -0
- package/apps/storefront/src/components/commerce/PriceDisplay.astro +91 -0
- package/apps/storefront/src/components/commerce/ProductCarousel.astro +197 -0
- package/apps/storefront/src/components/commerce/ProductGrid.astro +75 -0
- package/apps/storefront/src/components/commerce/RatingStars.astro +58 -0
- package/apps/storefront/src/components/compliance/NicotineWarning.astro +33 -0
- package/apps/storefront/src/components/home/Hero.astro +164 -0
- package/apps/storefront/src/components/islands/AgeGate.tsx +105 -0
- package/apps/storefront/src/components/islands/ImageGallery.tsx +237 -0
- package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +73 -0
- package/apps/storefront/src/components/islands/MobileNav.tsx +239 -0
- package/apps/storefront/src/components/islands/QuickView.tsx +138 -0
- package/apps/storefront/src/components/islands/RecentlyViewed.tsx +122 -0
- package/apps/storefront/src/components/islands/SavedList.tsx +99 -0
- package/apps/storefront/src/components/islands/SearchTypeahead.tsx +248 -0
- package/apps/storefront/src/components/islands/VariantSelector.tsx +378 -0
- package/apps/storefront/src/components/marketing/CardGrid.astro +72 -0
- package/apps/storefront/src/components/marketing/CodeSample.astro +20 -0
- package/apps/storefront/src/components/marketing/CtaBand.astro +24 -0
- package/apps/storefront/src/components/marketing/Faq.astro +37 -0
- package/apps/storefront/src/components/marketing/Integrations.astro +63 -0
- package/apps/storefront/src/components/marketing/MarketingBlocks.astro +53 -0
- package/apps/storefront/src/components/marketing/MarketingCtas.astro +48 -0
- package/apps/storefront/src/components/marketing/MarketingHero.astro +72 -0
- package/apps/storefront/src/components/marketing/ProofStrip.astro +37 -0
- package/apps/storefront/src/components/marketing/SplitCompare.astro +53 -0
- package/apps/storefront/src/components/marketing/Steps.astro +36 -0
- package/apps/storefront/src/components/marketing/Testimonials.astro +38 -0
- package/apps/storefront/src/components/marketing/TrustBar.astro +22 -0
- package/apps/storefront/src/components/nav/Footer.astro +148 -0
- package/apps/storefront/src/components/nav/Header.astro +190 -0
- package/apps/storefront/src/components/plp/ActiveFilters.astro +79 -0
- package/apps/storefront/src/components/plp/FacetSidebar.astro +146 -0
- package/apps/storefront/src/components/plp/Pagination.astro +99 -0
- package/apps/storefront/src/components/plp/SortSelect.astro +64 -0
- package/apps/storefront/src/config/devTenants.ts +16 -0
- package/apps/storefront/src/config/kvTenantResolver.ts +87 -0
- package/apps/storefront/src/config/resolver.ts +46 -0
- package/apps/storefront/src/config/tenants.ts +193 -0
- package/apps/storefront/src/layouts/Layout.astro +303 -0
- package/apps/storefront/src/lib/analytics/index.ts +47 -0
- package/apps/storefront/src/lib/auth/brokerAssertion.ts +136 -0
- package/apps/storefront/src/lib/auth/devAuth.ts +90 -0
- package/apps/storefront/src/lib/auth/gatePage.ts +81 -0
- package/apps/storefront/src/lib/auth/identityToken.ts +341 -0
- package/apps/storefront/src/lib/auth/loginGate.ts +107 -0
- package/apps/storefront/src/lib/auth/route.ts +146 -0
- package/apps/storefront/src/lib/auth/session.ts +203 -0
- package/apps/storefront/src/lib/auth/totAccessClient.ts +503 -0
- package/apps/storefront/src/lib/basePath.ts +41 -0
- package/apps/storefront/src/lib/cfImage.ts +55 -0
- package/apps/storefront/src/lib/chrome/model.ts +65 -0
- package/apps/storefront/src/lib/colors.ts +97 -0
- package/apps/storefront/src/lib/content-edit/client.ts +230 -0
- package/apps/storefront/src/lib/content-edit/index.ts +14 -0
- package/apps/storefront/src/lib/content-edit/route.ts +25 -0
- package/apps/storefront/src/lib/content-edit/types.ts +19 -0
- package/apps/storefront/src/lib/cspStaticHashes.ts +24 -0
- package/apps/storefront/src/lib/d1/catalog.ts +289 -0
- package/apps/storefront/src/lib/dashboard/staffAdmission.ts +43 -0
- package/apps/storefront/src/lib/dashboard/staffVendorAccess.ts +449 -0
- package/apps/storefront/src/lib/dashboard/tenantSelection.ts +136 -0
- package/apps/storefront/src/lib/dashboard/vendorQuery.ts +61 -0
- package/apps/storefront/src/lib/demoGate.ts +113 -0
- package/apps/storefront/src/lib/dev/ai-widget-client.js +127 -0
- package/apps/storefront/src/lib/dev/cliSignInCode.ts +132 -0
- package/apps/storefront/src/lib/diag/redact.ts +20 -0
- package/apps/storefront/src/lib/edgeCache.ts +128 -0
- package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +62 -0
- package/apps/storefront/src/lib/env.ts +60 -0
- package/apps/storefront/src/lib/fixtures/sampleProducts.ts +130 -0
- package/apps/storefront/src/lib/format.ts +29 -0
- package/apps/storefront/src/lib/foxyCommerce.ts +49 -0
- package/apps/storefront/src/lib/imageAttrs.ts +55 -0
- package/apps/storefront/src/lib/jsonld.ts +136 -0
- package/apps/storefront/src/lib/maintenance.ts +71 -0
- package/apps/storefront/src/lib/memoryKv.ts +32 -0
- package/apps/storefront/src/lib/messaging/messagesClient.ts +143 -0
- package/apps/storefront/src/lib/placeholder.ts +99 -0
- package/apps/storefront/src/lib/previewReload.ts +125 -0
- package/apps/storefront/src/lib/rawMarketingHtml.ts +133 -0
- package/apps/storefront/src/lib/search/index.ts +74 -0
- package/apps/storefront/src/lib/sections.ts +129 -0
- package/apps/storefront/src/lib/securityHeaders.ts +31 -0
- package/apps/storefront/src/lib/storyblok/content-model.ts +289 -0
- package/apps/storefront/src/lib/storyblok/provider.ts +524 -0
- package/apps/storefront/src/lib/tenantRouting.ts +91 -0
- package/apps/storefront/src/lib/tot/ToTClient.ts +177 -0
- package/apps/storefront/src/lib/tot/d1Client.ts +62 -0
- package/apps/storefront/src/lib/tot/query.ts +187 -0
- package/apps/storefront/src/lib/tot/totClientInterface.ts +30 -0
- package/apps/storefront/src/lib/url.ts +119 -0
- package/apps/storefront/src/lib/wishlist.ts +63 -0
- package/apps/storefront/src/middleware/index.ts +413 -0
- package/apps/storefront/src/pages/404.astro +61 -0
- package/apps/storefront/src/pages/[...slug].astro +90 -0
- package/apps/storefront/src/pages/api/auth/logout.ts +21 -0
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +107 -0
- package/apps/storefront/src/pages/api/auth/send.ts +85 -0
- package/apps/storefront/src/pages/api/auth/verify.ts +135 -0
- package/apps/storefront/src/pages/api/cache-purge.ts +44 -0
- package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +64 -0
- package/apps/storefront/src/pages/api/csp-report.ts +25 -0
- package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +124 -0
- package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +120 -0
- package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +44 -0
- package/apps/storefront/src/pages/api/newsletter.ts +104 -0
- package/apps/storefront/src/pages/api/request-access.ts +172 -0
- package/apps/storefront/src/pages/auth/login.astro +149 -0
- package/apps/storefront/src/pages/auth/magic.astro +105 -0
- package/apps/storefront/src/pages/capabilities.astro +292 -0
- package/apps/storefront/src/pages/collections/[handle].astro +154 -0
- package/apps/storefront/src/pages/collections/index.astro +59 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +100 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +154 -0
- package/apps/storefront/src/pages/dashboard/index.astro +160 -0
- package/apps/storefront/src/pages/dev.astro +653 -0
- package/apps/storefront/src/pages/index.astro +290 -0
- package/apps/storefront/src/pages/llms.txt.ts +92 -0
- package/apps/storefront/src/pages/products/[handle].astro +443 -0
- package/apps/storefront/src/pages/robots.txt.ts +24 -0
- package/apps/storefront/src/pages/saved.astro +35 -0
- package/apps/storefront/src/pages/search.astro +150 -0
- package/apps/storefront/src/pages/sitemap.xml.ts +78 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +646 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +85 -0
- package/apps/storefront/src/pages/style-guide/index.astro +94 -0
- package/apps/storefront/src/pages/tenants/[id]/[...path].ts +57 -0
- package/apps/storefront/src/styles/global.css +277 -0
- package/apps/storefront/src/themes/reference.ts +61 -0
- package/apps/storefront/src/themes/schema.ts +111 -0
- package/apps/storefront/src/themes/tenants/index.ts +51 -0
- package/apps/storefront/tsconfig.json +16 -0
- package/apps/storefront/vitest.config.ts +22 -0
- package/apps/storefront/wrangler.toml +175 -0
- package/package.json +23 -0
- package/packages/public-runtime/LICENSE +58 -0
- package/packages/public-runtime/package.json +22 -0
- package/packages/public-runtime/src/binary-path.ts +68 -0
- package/packages/public-runtime/src/capabilities.ts +89 -0
- package/packages/public-runtime/src/catalog-api.ts +81 -0
- package/packages/public-runtime/src/catalog-d1.ts +192 -0
- package/packages/public-runtime/src/csp.ts +109 -0
- package/packages/public-runtime/src/customization-preview.ts +197 -0
- package/packages/public-runtime/src/customization-reconcile.ts +63 -0
- package/packages/public-runtime/src/customization-runtime.ts +157 -0
- package/packages/public-runtime/src/customization-scripts.ts +64 -0
- package/packages/public-runtime/src/customization-versioning.ts +95 -0
- package/packages/public-runtime/src/foxy.ts +184 -0
- package/packages/public-runtime/src/index.ts +30 -0
- package/packages/public-runtime/src/product.ts +159 -0
- package/packages/public-runtime/src/search.ts +45 -0
- package/packages/public-runtime/src/tenant-assets.ts +444 -0
- package/packages/public-runtime/src/tenant.ts +243 -0
- package/packages/public-runtime/tsconfig.json +8 -0
- package/packages/public-runtime/vitest.config.ts +8 -0
- package/pnpm-workspace.yaml +7 -0
- package/scripts/build/copy-tenant-assets.mjs +69 -0
- package/scripts/tot-dev.mjs +439 -0
- package/tenants/bigdvapor/content/blog.json +52 -0
- package/tenants/bigdvapor/content/chrome.html +149 -0
- package/tenants/bigdvapor/content/chrome.json +47 -0
- package/tenants/bigdvapor/content/home.html +463 -0
- package/tenants/bigdvapor/content/home.json +73 -0
- package/tenants/bigdvapor/content/pages/about.json +5 -0
- package/tenants/bigdvapor/content/pages/privacy.json +6 -0
- package/tenants/bigdvapor/content/pages/shipping-returns.json +6 -0
- package/tenants/bigdvapor/content/pages-html/blogs/news.html +86 -0
- package/tenants/bigdvapor/content/pages-html/pages/about-us.html +106 -0
- package/tenants/bigdvapor/content/pages-html/pages/contact-us.html +61 -0
- package/tenants/bigdvapor/content/pages-html/pages/privacy-policy.html +72 -0
- package/tenants/bigdvapor/content/pages-html/pages/shipping-returns.html +172 -0
- package/tenants/bigdvapor/content/themes/bigd-navy.json +109 -0
- package/tenants/bigdvapor/public/img/brands/adjust-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/again-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/al-fakher-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/arro-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/cookies-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/core-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/cyclone-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/dinner-lady-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/extre-bar-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/fifty-bar-vape-disposable.jpg +0 -0
- package/tenants/bigdvapor/public/img/brands/firerose-vape-disposable.jpg +0 -0
- package/tenants/bigdvapor/public/img/brands/flavor-beast-vape-disposable.jpg +0 -0
- package/tenants/bigdvapor/public/img/brands/foger-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/kk-energy-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/kumi-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/lost-vape-dispoable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/melo-labs-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/brands/mixo-vape-disposable.webp +0 -0
- package/tenants/bigdvapor/public/img/brands/one-tank-vape-disposable.jpg +0 -0
- package/tenants/bigdvapor/public/img/brands/oxbar-vape-disposable.webp +0 -0
- package/tenants/bigdvapor/public/img/brands/pillow-talk-vape-disposable.jpg +0 -0
- package/tenants/bigdvapor/public/img/brands/pod-king-vape-disposable.jpg +0 -0
- package/tenants/bigdvapor/public/img/brands/raz-vape-disposable.png +0 -0
- package/tenants/bigdvapor/public/img/hero-texas.jpg +0 -0
- package/tenants/bigdvapor/public/img/og.jpg +0 -0
- package/tenants/bigdvapor/public/img/pages/AdobeStock_202106312_web.jpg +0 -0
- package/tenants/bigdvapor/public/img/pages/CREW_web.jpg +0 -0
- package/tenants/bigdvapor/public/img/pages/Hero-Image.jpg +0 -0
- package/tenants/bigdvapor/public/img/tiles/adjust.png +0 -0
- package/tenants/bigdvapor/public/img/tiles/again.png +0 -0
- package/tenants/bigdvapor/public/img/tiles/al-fakher.png +0 -0
- package/tenants/bigdvapor/public/img/tiles/arro.png +0 -0
- package/tenants/bigdvapor/public/img/tiles/cookies.png +0 -0
- package/tenants/bigdvapor/public/img/tiles/core.png +0 -0
- package/tenants/bigdvapor/public/img/tiles/cyclone.png +0 -0
- package/tenants/bigdvapor/public/img/tiles/disposables.svg +23 -0
- package/tenants/bigdvapor/public/logo-wordmark.png +0 -0
- package/tenants/bigdvapor/public/pages/home.css +118 -0
- package/tenants/bigdvapor/public/pages/mkt.css +185 -0
- package/tenants/bigdvapor/public/pages/page.css +148 -0
- package/tenants/bigdvapor/public/themes/bigd-navy.css +73 -0
- package/tenants/bigdvapor/theme.json +12 -0
- package/tenants/giantvapes/content/chrome.html +152 -0
- package/tenants/giantvapes/content/chrome.json +94 -0
- package/tenants/giantvapes/content/home.html +194 -0
- package/tenants/giantvapes/content/home.json +50 -0
- package/tenants/giantvapes/content/pages/about.json +10 -0
- package/tenants/giantvapes/content/pages/privacy.json +6 -0
- package/tenants/giantvapes/content/pages/shipping-returns.json +6 -0
- package/tenants/giantvapes/content/pages-html/blogs/news.html +68 -0
- package/tenants/giantvapes/content/pages-html/pages/about-us.html +95 -0
- package/tenants/giantvapes/content/pages-html/pages/contact-us.html +68 -0
- package/tenants/giantvapes/content/pages-html/pages/privacy-policy.html +65 -0
- package/tenants/giantvapes/content/pages-html/pages/shipping-returns.html +77 -0
- package/tenants/giantvapes/content/themes/giant-navy.json +73 -0
- package/tenants/giantvapes/public/img/hero-suicide-bunny.jpg +0 -0
- package/tenants/giantvapes/public/img/hero.webp +0 -0
- package/tenants/giantvapes/public/img/og.jpg +0 -0
- package/tenants/giantvapes/public/logo-wordmark.png +0 -0
- package/tenants/giantvapes/public/pages/home.css +120 -0
- package/tenants/giantvapes/public/pages/mkt.css +185 -0
- package/tenants/giantvapes/public/pages/page.css +155 -0
- package/tenants/giantvapes/public/themes/giant-navy.css +76 -0
- package/tenants/giantvapes/theme.json +39 -0
- package/tenants/home/content/chrome.json +11 -0
- package/tenants/home/content/home.html +304 -0
- package/tenants/home/content/home.json +13 -0
- package/tenants/home/public/js/storefront.js +210 -0
- package/tenants/home/public/logo-wordmark.png +0 -0
- package/tenants/home/public/pages/storefront.css +212 -0
- package/tenants/home/theme.json +6 -0
- package/tenants/tokenoftrust/.tot/config.json +9 -0
- package/tenants/tokenoftrust/content/chrome.json +55 -0
- package/tenants/tokenoftrust/content/home.html +723 -0
- package/tenants/tokenoftrust/content/home.json +233 -0
- package/tenants/tokenoftrust/content/pages-html/careers.html +231 -0
- package/tenants/tokenoftrust/content/pages-html/company.html +275 -0
- package/tenants/tokenoftrust/content/pages-html/contact/contact-sales.html +188 -0
- package/tenants/tokenoftrust/content/pages-html/contact/startup-apply.html +206 -0
- package/tenants/tokenoftrust/content/pages-html/contact.html +178 -0
- package/tenants/tokenoftrust/content/pages-html/industries/adult-content-age-verification.html +286 -0
- package/tenants/tokenoftrust/content/pages-html/industries/cannabis.html +277 -0
- package/tenants/tokenoftrust/content/pages-html/pact-act-pricing.html +289 -0
- package/tenants/tokenoftrust/content/pages-html/pricing.html +483 -0
- package/tenants/tokenoftrust/content/pages-html/product/age-assurance/age-estimation.html +278 -0
- package/tenants/tokenoftrust/content/pages-html/product/age-assurance/age-gate.html +279 -0
- package/tenants/tokenoftrust/content/pages-html/product/age-assurance/age-verification.html +265 -0
- package/tenants/tokenoftrust/content/pages-html/product/age-assurance.html +173 -0
- package/tenants/tokenoftrust/content/pages-html/product/compliance/aml.html +214 -0
- package/tenants/tokenoftrust/content/pages-html/product/compliance/pact-act.html +226 -0
- package/tenants/tokenoftrust/content/pages-html/product/fraud-prevention.html +254 -0
- package/tenants/tokenoftrust/content/pages-html/product/social-profile-verification.html +212 -0
- package/tenants/tokenoftrust/content/pages-html/product/tax/excise-tax.html +213 -0
- package/tenants/tokenoftrust/content/pages-html/product/verification/aml.html +259 -0
- package/tenants/tokenoftrust/content/pages-html/product/verification/biometric-selfie.html +305 -0
- package/tenants/tokenoftrust/content/pages-html/product/verification/coverage.html +254 -0
- package/tenants/tokenoftrust/content/pages-html/product/verification/document-verification.html +306 -0
- package/tenants/tokenoftrust/content/pages-html/product/verification/electronic-identity-verification.html +304 -0
- package/tenants/tokenoftrust/content/pages-html/product/verification/government-id-verification.html +308 -0
- package/tenants/tokenoftrust/content/pages-html/product/verification.html +183 -0
- package/tenants/tokenoftrust/content/pages-html/product.html +228 -0
- package/tenants/tokenoftrust/content/pages-html/resources/integrations/bigcommerce-integration.html +220 -0
- package/tenants/tokenoftrust/content/pages-html/resources/integrations/shopify-integration.html +221 -0
- package/tenants/tokenoftrust/content/pages-html/resources/integrations/wordpress.html +220 -0
- package/tenants/tokenoftrust/content/pages-html/resources/integrations.html +172 -0
- package/tenants/tokenoftrust/content/pages-html/resources/refer-a-friend-program.html +317 -0
- package/tenants/tokenoftrust/content/pages-html/resources.html +177 -0
- package/tenants/tokenoftrust/content/pages-html/reviews.html +273 -0
- package/tenants/tokenoftrust/content/pages-html/solutions.html +292 -0
- package/tenants/tokenoftrust/content/pages-html/vlp-alcohol.html +289 -0
- package/tenants/tokenoftrust/content/pages-html/vlp-cigars-and-tobacco.html +286 -0
- package/tenants/tokenoftrust/content/pages-html/vlp-firearm-accessories.html +267 -0
- package/tenants/tokenoftrust/content/pages-html/vlp-peptides.html +273 -0
- package/tenants/tokenoftrust/content/themes/tot-navy.json +88 -0
- package/tenants/tokenoftrust/public/favicon.png +0 -0
- package/tenants/tokenoftrust/public/logo-icon.png +0 -0
- package/tenants/tokenoftrust/public/logo-wordmark.png +0 -0
- package/tenants/tokenoftrust/public/pages/company.css +108 -0
- package/tenants/tokenoftrust/public/pages/home.css +457 -0
- package/tenants/tokenoftrust/public/pages/mkt.css +482 -0
- package/tenants/tokenoftrust/public/pages/pricing.css +214 -0
- package/tenants/tokenoftrust/public/pages/solutions.css +66 -0
- package/tenants/tokenoftrust/public/themes/tot-navy-1.0.0.css +136 -0
- package/tenants/tokenoftrust/public/themes/tot-navy.css +358 -0
- package/tenants/tokenoftrust/theme.json +31 -0
- package/tsconfig.base.json +20 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant-resolution middleware (brief §7).
|
|
3
|
+
*
|
|
4
|
+
* Per request: Host -> TenantConfig (via TenantResolver) -> resolve theme ->
|
|
5
|
+
* bind a ToTClient to the tenant's data scope -> stash everything on
|
|
6
|
+
* Astro.locals so pages/components stay tenant-agnostic. The serialized theme
|
|
7
|
+
* CSS-var block is injected into <head> by Layout.astro from locals.themeStyle.
|
|
8
|
+
*/
|
|
9
|
+
import { defineMiddleware } from "astro:middleware";
|
|
10
|
+
import { getTenantResolver } from "@/config/resolver";
|
|
11
|
+
import { referenceTheme } from "@/themes/reference";
|
|
12
|
+
import { mergeTheme, themeToCssVars } from "@/themes/schema";
|
|
13
|
+
import { createToTClient } from "@/lib/tot/ToTClient";
|
|
14
|
+
import { readEnv, readKv, readD1 } from "@/lib/env";
|
|
15
|
+
import { resolveTenantRouting } from "@/lib/tenantRouting";
|
|
16
|
+
import { createContentProvider } from "@/lib/storyblok/provider";
|
|
17
|
+
import {
|
|
18
|
+
isCacheablePath,
|
|
19
|
+
pageCacheKey,
|
|
20
|
+
readCacheVersion,
|
|
21
|
+
getCachedPage,
|
|
22
|
+
putCachedPage,
|
|
23
|
+
} from "@/lib/edgeCache";
|
|
24
|
+
import { STATIC_SCRIPT_HASHES, STATIC_STYLE_HASHES } from "@/lib/cspStaticHashes";
|
|
25
|
+
import { applySecurityHeaders } from "@/lib/securityHeaders";
|
|
26
|
+
import { maybeInjectPreviewReload } from "@/lib/previewReload";
|
|
27
|
+
import {
|
|
28
|
+
resolveCustomizationEnv,
|
|
29
|
+
buildCsp,
|
|
30
|
+
STOREFRONT_CSP_ALLOWLIST,
|
|
31
|
+
foxyCspAdditions,
|
|
32
|
+
CSP_REPORT_GROUP,
|
|
33
|
+
CSP_REPORT_PATH,
|
|
34
|
+
readPublishedArtifact,
|
|
35
|
+
themeArtifactPath,
|
|
36
|
+
resolveTenantFeatures,
|
|
37
|
+
resolveCapabilities,
|
|
38
|
+
capabilitiesArtifactPath,
|
|
39
|
+
} from "@tot/public-runtime";
|
|
40
|
+
import type { CapabilityConfig } from "@tot/public-runtime";
|
|
41
|
+
import { resolveTenantCommerce } from "@/lib/foxyCommerce";
|
|
42
|
+
import { evaluateDemoGate, hostInGateList } from "@/lib/demoGate";
|
|
43
|
+
import {
|
|
44
|
+
evaluateLoginGate,
|
|
45
|
+
parseGateMode,
|
|
46
|
+
isAuthPath,
|
|
47
|
+
sessionAdmitsHost,
|
|
48
|
+
PUBLIC_HOME_TENANT_ID,
|
|
49
|
+
} from "@/lib/auth/loginGate";
|
|
50
|
+
import { gatedHoldingResponse } from "@/lib/auth/gatePage";
|
|
51
|
+
import { readSession, KvSessionStore, SESSION_COOKIE } from "@/lib/auth/session";
|
|
52
|
+
import { isMaintenanceOn, maintenanceResponse } from "@/lib/maintenance";
|
|
53
|
+
import { membershipFor } from "@/lib/dashboard/tenantSelection";
|
|
54
|
+
import type { DeepPartial, ThemeTokens } from "@tot/public-runtime";
|
|
55
|
+
|
|
56
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
57
|
+
const { request, locals } = context;
|
|
58
|
+
const url = new URL(request.url);
|
|
59
|
+
const host =
|
|
60
|
+
request.headers.get("host") ?? url.host ?? "localhost";
|
|
61
|
+
|
|
62
|
+
// ---- Site maintenance kill-switch (fail-safe full close) ------------------
|
|
63
|
+
// MAINTENANCE_MODE=1 takes the ENTIRE site offline (every host + path) with a
|
|
64
|
+
// static "Coming Soon" holding page, BEFORE tenant resolution, the access
|
|
65
|
+
// gate, the edge cache, or any render. Not a login gate — auth paths are not
|
|
66
|
+
// exempt. Back online: set MAINTENANCE_MODE=0 (or unset) and redeploy.
|
|
67
|
+
if (isMaintenanceOn(await readEnv("MAINTENANCE_MODE"))) {
|
|
68
|
+
return maintenanceResponse();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Demo access gate for named hosts (DEMO_GATE_HOSTS). Two modes, switched by
|
|
72
|
+
// DEMO_GATE_MODE:
|
|
73
|
+
// "password" (default) — interim Basic-Auth gate, enforced HERE (host-level,
|
|
74
|
+
// no tenant/session needed) so a blocked host never reaches tenant
|
|
75
|
+
// resolution, the edge cache, or any render.
|
|
76
|
+
// "login" — "Sign in with ToT" gate, enforced just below (needs the resolved
|
|
77
|
+
// tenant as the grant `resource` + the SESSION KV), redirecting to
|
|
78
|
+
// /auth/login unless a valid, resource-bound tot_session cookie is present.
|
|
79
|
+
const gateMode = parseGateMode(await readEnv("DEMO_GATE_MODE"));
|
|
80
|
+
const demoGateHosts = await readEnv("DEMO_GATE_HOSTS");
|
|
81
|
+
if (gateMode === "password") {
|
|
82
|
+
const demoGate = evaluateDemoGate(host, request.headers.get("authorization"), {
|
|
83
|
+
hosts: demoGateHosts,
|
|
84
|
+
password: await readEnv("DEMO_GATE_PASSWORD"),
|
|
85
|
+
realm: await readEnv("DEMO_GATE_REALM"),
|
|
86
|
+
});
|
|
87
|
+
if (demoGate) return demoGate;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Host first; if the host owns no tenant, a leading /<domain>/ path segment
|
|
91
|
+
// selects one (DNS-free tenant selection on localhost/preview). The resolver
|
|
92
|
+
// is KV-backed in prod (many-tenant) and static in plain dev.
|
|
93
|
+
const resolver = await getTenantResolver();
|
|
94
|
+
const { tenant, basePath, renderPath } = await resolveTenantRouting(
|
|
95
|
+
host,
|
|
96
|
+
url.pathname,
|
|
97
|
+
resolver,
|
|
98
|
+
);
|
|
99
|
+
if (!tenant) {
|
|
100
|
+
return new Response("Unknown host", { status: 404 });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Workers Cache API: backs the full-page edge cache below. Undefined in
|
|
104
|
+
// `astro dev` (Node) — every use is a no-op there.
|
|
105
|
+
const cache =
|
|
106
|
+
typeof caches !== "undefined" && "default" in caches
|
|
107
|
+
? (caches as unknown as { default: Cache }).default
|
|
108
|
+
: undefined;
|
|
109
|
+
const kv = await readKv("TENANT_CACHE");
|
|
110
|
+
|
|
111
|
+
// Login-gate enforcement (DEMO_GATE_MODE="login"): runs before the edge cache
|
|
112
|
+
// so a gated page is never served to an unauthenticated viewer. The platform's
|
|
113
|
+
// own "self" home tenant is EXEMPT — it renders publicly (browse + sign up);
|
|
114
|
+
// every other tenant on a gated host is withheld behind sign-in. A valid,
|
|
115
|
+
// resource-bound tot_session sets locals.viewer (email + roles) for chrome;
|
|
116
|
+
// otherwise an unauthenticated visitor gets a "Coming soon" holding page with
|
|
117
|
+
// a Sign-in button (auth paths are exempt so sign-in itself is reachable). The
|
|
118
|
+
// grant `resource` is the tenant's appDomain — never input.
|
|
119
|
+
const tenantExempt = tenant.tenant_id === PUBLIC_HOME_TENANT_ID;
|
|
120
|
+
if (gateMode === "login" && hostInGateList(host, demoGateHosts)) {
|
|
121
|
+
// UNIFIED on the federated session: read the FULL session (no resource filter —
|
|
122
|
+
// federated sign-in's aud is the storefront app, not this tenant) and admit the
|
|
123
|
+
// viewer iff they hold a MEMBERSHIP for the host's tenant. One session model for
|
|
124
|
+
// the console + the preview gate. Deny-closed: no session / no membership → gate.
|
|
125
|
+
let viewerRecord = null;
|
|
126
|
+
if (!isAuthPath(url.pathname)) {
|
|
127
|
+
const sessionKv = await readKv("SESSION");
|
|
128
|
+
if (sessionKv) {
|
|
129
|
+
const sid = context.cookies.get(SESSION_COOKIE)?.value ?? null;
|
|
130
|
+
viewerRecord = await readSession(
|
|
131
|
+
new KvSessionStore(sessionKv),
|
|
132
|
+
sid,
|
|
133
|
+
Math.floor(Date.now() / 1000),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
138
|
+
const admitted = sessionAdmitsHost(viewerRecord, tenant.appDomain, nowSeconds);
|
|
139
|
+
const decision = evaluateLoginGate({
|
|
140
|
+
host,
|
|
141
|
+
pathname: url.pathname,
|
|
142
|
+
search: url.search,
|
|
143
|
+
mode: gateMode,
|
|
144
|
+
gatedHosts: demoGateHosts,
|
|
145
|
+
hasValidSession: admitted,
|
|
146
|
+
tenantExempt,
|
|
147
|
+
});
|
|
148
|
+
if (decision.action === "redirect") {
|
|
149
|
+
// Withhold content but offer sign-in (carrying the intended destination).
|
|
150
|
+
return gatedHoldingResponse(decision.location);
|
|
151
|
+
}
|
|
152
|
+
if (admitted && viewerRecord) {
|
|
153
|
+
// Surface the viewer's role FOR THIS host's tenant as chrome. A member sees
|
|
154
|
+
// their tenants[] role; a ToT-staff viewer admitted via an explicit vendor
|
|
155
|
+
// selection sees the staffRoles-scoped capability (distinct path).
|
|
156
|
+
const role = membershipFor(viewerRecord.tenants, tenant.appDomain, {
|
|
157
|
+
selection: viewerRecord.staffSelection,
|
|
158
|
+
nowSeconds,
|
|
159
|
+
})?.role;
|
|
160
|
+
locals.viewer = {
|
|
161
|
+
email: viewerRecord.email,
|
|
162
|
+
roles: role ? [role] : viewerRecord.roles,
|
|
163
|
+
capability: role ?? viewerRecord.capability,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Preview/test detection (also reused for the customization env below). A
|
|
169
|
+
// production read is one with no preview host, query token, or preview cookie.
|
|
170
|
+
const isPreviewHost = host.toLowerCase().startsWith("preview.");
|
|
171
|
+
const queryToken = url.searchParams.get("__preview");
|
|
172
|
+
const cookieToken = context.cookies.get("tot_preview")?.value ?? null;
|
|
173
|
+
|
|
174
|
+
// ---- Edge page cache (D1): short-circuit production browse reads on a hit,
|
|
175
|
+
// BEFORE building the client / theme / rendering. Misses store the final
|
|
176
|
+
// response at the end of this middleware.
|
|
177
|
+
//
|
|
178
|
+
// NEVER in dev (`astro dev`): the workerd dev runtime persists the Workers Cache
|
|
179
|
+
// API in .wrangler/state, so a cached page would be served stale across edits AND
|
|
180
|
+
// restarts — silently defeating hot-reload (edits look "lost"). Dev must always
|
|
181
|
+
// re-render. import.meta.env.DEV statically drops this guard from the prod build.
|
|
182
|
+
const pageCacheEligible =
|
|
183
|
+
!import.meta.env.DEV &&
|
|
184
|
+
request.method.toUpperCase() === "GET" &&
|
|
185
|
+
!isPreviewHost &&
|
|
186
|
+
!queryToken &&
|
|
187
|
+
!cookieToken &&
|
|
188
|
+
isCacheablePath(url.pathname, url.searchParams);
|
|
189
|
+
let pageKey: Request | null = null;
|
|
190
|
+
if (pageCacheEligible && cache && kv) {
|
|
191
|
+
const version = await readCacheVersion(kv, tenant.tenant_id);
|
|
192
|
+
pageKey = pageCacheKey(tenant.tenant_id, url.pathname, url.search, version);
|
|
193
|
+
const hit = await getCachedPage(cache, pageKey);
|
|
194
|
+
if (hit) return hit;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Edge read replica: opt-in via USE_D1_CATALOG=1 (+ a CATALOG_DB binding). Off
|
|
198
|
+
// by default, so the bundled fixture snapshot serves reads.
|
|
199
|
+
const d1 =
|
|
200
|
+
(await readEnv("USE_D1_CATALOG")) === "1" ? await readD1("CATALOG_DB") : undefined;
|
|
201
|
+
const tot = await createToTClient(tenant, d1);
|
|
202
|
+
|
|
203
|
+
const proto = url.protocol === "http:" ? "http" : "https";
|
|
204
|
+
// Canonical base uses the tenant's canonical domain in prod; for local dev we
|
|
205
|
+
// keep the actual host (plus any /<domain> base path) so links resolve.
|
|
206
|
+
const isLocal = host.startsWith("localhost") || host.startsWith("127.0.0.1");
|
|
207
|
+
const canonicalBase = isLocal
|
|
208
|
+
? `${proto}://${host}${basePath}`
|
|
209
|
+
: `https://${tenant.canonicalDomain}`;
|
|
210
|
+
|
|
211
|
+
// Live vs test customization (D4). Production hosts always serve "live" and
|
|
212
|
+
// ignore any token; a "preview." host is auth-gated by a signed, tenant-bound,
|
|
213
|
+
// short-TTL preview token (?__preview= on first hit, then the tot_preview
|
|
214
|
+
// cookie). Test is never indexable (X-Robots-Tag below).
|
|
215
|
+
const previewToken = queryToken ?? cookieToken;
|
|
216
|
+
const customization = await resolveCustomizationEnv({
|
|
217
|
+
isPreviewHost,
|
|
218
|
+
tenantId: tenant.tenant_id,
|
|
219
|
+
token: previewToken,
|
|
220
|
+
secret: (await readEnv("CUSTOMIZATION_PREVIEW_SECRET")) ?? null,
|
|
221
|
+
nowSeconds: Math.floor(Date.now() / 1000),
|
|
222
|
+
});
|
|
223
|
+
if (customization.reject) {
|
|
224
|
+
return new Response(customization.reject.message, {
|
|
225
|
+
status: customization.reject.status,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
// Persist a query-supplied token as a cookie so later navigations stay in test.
|
|
229
|
+
if (customization.env === "test" && queryToken) {
|
|
230
|
+
context.cookies.set("tot_preview", queryToken, {
|
|
231
|
+
httpOnly: true,
|
|
232
|
+
secure: true,
|
|
233
|
+
sameSite: "lax",
|
|
234
|
+
path: "/",
|
|
235
|
+
maxAge: 3600,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Apply a published theme override for the resolved environment (store->app).
|
|
240
|
+
// The live domain serves `live`; a gated preview serves `test`. When nothing
|
|
241
|
+
// is published the build-time tenant theme is used unchanged.
|
|
242
|
+
//
|
|
243
|
+
// Resolution is versioned-aware inside readPublishedArtifact: env maps to a
|
|
244
|
+
// publish channel (live->live, test->preview); if the tenant has a channel
|
|
245
|
+
// pointer, the artifact is resolved through the immutable site version
|
|
246
|
+
// (channel -> versionId -> manifest -> content). Tenants with no channel
|
|
247
|
+
// pointer fall back to the legacy per-path selection — no behavior change.
|
|
248
|
+
const kvGet = kv ? (key: string) => kv.get(key) : async () => null;
|
|
249
|
+
let theme = mergeTheme(referenceTheme, tenant.theme_tokens);
|
|
250
|
+
const publishedTheme = await readPublishedArtifact(
|
|
251
|
+
kvGet,
|
|
252
|
+
tenant.tenant_id,
|
|
253
|
+
customization.env,
|
|
254
|
+
themeArtifactPath(tenant.tenant_id),
|
|
255
|
+
);
|
|
256
|
+
if (publishedTheme) {
|
|
257
|
+
try {
|
|
258
|
+
theme = mergeTheme(theme, JSON.parse(publishedTheme) as DeepPartial<ThemeTokens>);
|
|
259
|
+
} catch {
|
|
260
|
+
/* malformed published artifact — keep the build-time theme */
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Per-environment capabilities (Track A/B). Config-as-code: the build-time
|
|
265
|
+
// TenantConfig default is overridden by the published capabilities artifact for
|
|
266
|
+
// THIS environment — materialized from the tenant repo's .tot/config.json per
|
|
267
|
+
// commit (localhost=working tree, preview=PR head, live=promoted). So a
|
|
268
|
+
// capability change is PR-gated and separable per environment.
|
|
269
|
+
let publishedCaps: Partial<CapabilityConfig> | null = null;
|
|
270
|
+
const publishedCapsRaw = await readPublishedArtifact(
|
|
271
|
+
kvGet,
|
|
272
|
+
tenant.tenant_id,
|
|
273
|
+
customization.env,
|
|
274
|
+
capabilitiesArtifactPath(tenant.tenant_id),
|
|
275
|
+
);
|
|
276
|
+
if (publishedCapsRaw) {
|
|
277
|
+
try {
|
|
278
|
+
publishedCaps = JSON.parse(publishedCapsRaw) as Partial<CapabilityConfig>;
|
|
279
|
+
} catch {
|
|
280
|
+
/* malformed published artifact — keep the build-time default */
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const capabilities = resolveCapabilities(tenant, publishedCaps);
|
|
284
|
+
|
|
285
|
+
// Per-request CSP nonce (D2): goes on the platform's own inline tags in
|
|
286
|
+
// Layout.astro so a strict CSP can allow them without 'unsafe-inline'.
|
|
287
|
+
const cspNonce = crypto.randomUUID();
|
|
288
|
+
|
|
289
|
+
locals.tenant = tenant;
|
|
290
|
+
// Effective capability flags for this tenant (commerce defaults unless the
|
|
291
|
+
// tenant declares siteType:"marketing" / explicit features). Pages and chrome
|
|
292
|
+
// read these instead of hard-coding tenant identity checks.
|
|
293
|
+
locals.features = resolveTenantFeatures(tenant);
|
|
294
|
+
locals.capabilities = capabilities;
|
|
295
|
+
// FoxyCart cart/checkout facts (Track A) — only when the tenant has a catalog
|
|
296
|
+
// AND the cartCheckout capability is enabled for THIS environment. Drives the
|
|
297
|
+
// loader + signed add-to-cart, and the CSP additions below.
|
|
298
|
+
const commerce =
|
|
299
|
+
locals.features.catalog && capabilities.cartCheckout.enabled
|
|
300
|
+
? await resolveTenantCommerce(tenant)
|
|
301
|
+
: null;
|
|
302
|
+
locals.commerce = commerce ?? undefined;
|
|
303
|
+
locals.tot = tot;
|
|
304
|
+
locals.content = createContentProvider(tenant, {
|
|
305
|
+
get: kvGet,
|
|
306
|
+
env: customization.env,
|
|
307
|
+
});
|
|
308
|
+
locals.themeStyle = themeToCssVars(theme);
|
|
309
|
+
locals.canonicalBase = canonicalBase;
|
|
310
|
+
locals.customizationEnv = customization.env;
|
|
311
|
+
locals.cspNonce = cspNonce;
|
|
312
|
+
// Prefix that path-routed pages live under ("" for host-routed). Pages stay
|
|
313
|
+
// tenant-agnostic; the Layout uses this to keep root-absolute links inside
|
|
314
|
+
// the tenant prefix on the client.
|
|
315
|
+
locals.basePath = basePath;
|
|
316
|
+
|
|
317
|
+
// When a /<domain>/ prefix selected the tenant, rewrite to the stripped route
|
|
318
|
+
// so the existing file-based routes match. next(path) rewrites WITHOUT
|
|
319
|
+
// re-running middleware, so the locals set above are preserved.
|
|
320
|
+
let response = basePath
|
|
321
|
+
? await next(`${renderPath}${url.search}`)
|
|
322
|
+
: await next();
|
|
323
|
+
|
|
324
|
+
// On-publish auto-refresh: inject the live-reload client into SHARED preview
|
|
325
|
+
// (`test`) HTML pages ONLY — never `live` (keeps live clean + edge-cacheable).
|
|
326
|
+
// It polls /__preview/version and reloads when this tenant's cache-version
|
|
327
|
+
// bumps on publish/reconcile. Injection happens before the CSP header is set so
|
|
328
|
+
// the script's per-request nonce (cspNonce) matches the enforced policy.
|
|
329
|
+
response = await maybeInjectPreviewReload(response, {
|
|
330
|
+
env: customization.env,
|
|
331
|
+
tenantId: tenant.tenant_id,
|
|
332
|
+
basePath,
|
|
333
|
+
nonce: cspNonce,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Baseline security headers (S1) on every response — set before the page-cache
|
|
337
|
+
// store below so cached copies carry them too.
|
|
338
|
+
applySecurityHeaders(response.headers);
|
|
339
|
+
|
|
340
|
+
// Preview (test) output must never be indexed.
|
|
341
|
+
if (customization.noindex) {
|
|
342
|
+
response.headers.set("X-Robots-Tag", "noindex, nofollow");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Strict CSP — now ENFORCED in every environment (Epic S2). The report-only
|
|
346
|
+
// rollout is complete: platform output is CSP-clean — inline styles are
|
|
347
|
+
// externalized (`inlineStylesheets: "never"`), Astro's static inline scripts +
|
|
348
|
+
// island style are hash-pinned (cspStaticHashes), our dynamic inline tags carry
|
|
349
|
+
// the per-request nonce, and the last inline event handler (SortSelect's
|
|
350
|
+
// `onchange`) is now a bundled 'self' script. Reporting endpoints stay attached
|
|
351
|
+
// (report-to/report-uri work in enforce mode), so we keep telemetry as tenant
|
|
352
|
+
// client JS arrives — those per-version tenant hashes extend `scriptSrc` per
|
|
353
|
+
// request (the mechanism below already supports it).
|
|
354
|
+
//
|
|
355
|
+
// DEV EXCEPTION: `astro dev` serves CSS/HMR as UN-nonced inline <style>/<script>
|
|
356
|
+
// (Vite injects them); the strict nonce policy blocks those, leaving the dev
|
|
357
|
+
// site unstyled. The BUILD is CSP-clean (styles are externalized via
|
|
358
|
+
// `inlineStylesheets:"never"`), so we enforce there and only REPORT in dev —
|
|
359
|
+
// violations are still surfaced, but styling/HMR work locally.
|
|
360
|
+
const cspHeader = import.meta.env.DEV
|
|
361
|
+
? "Content-Security-Policy-Report-Only"
|
|
362
|
+
: "Content-Security-Policy";
|
|
363
|
+
response.headers.set(
|
|
364
|
+
"Reporting-Endpoints",
|
|
365
|
+
`${CSP_REPORT_GROUP}="${CSP_REPORT_PATH}"`,
|
|
366
|
+
);
|
|
367
|
+
// FoxyCart origins (loader CDN + the store's cart/checkout host) added to the
|
|
368
|
+
// strict policy only when this tenant has a store — so add-to-cart, the
|
|
369
|
+
// sidecart iframe/XHR, and the cross-origin form POST all pass.
|
|
370
|
+
const foxyCsp = commerce ? foxyCspAdditions(commerce.origin) : null;
|
|
371
|
+
response.headers.set(
|
|
372
|
+
cspHeader,
|
|
373
|
+
buildCsp({
|
|
374
|
+
nonce: cspNonce,
|
|
375
|
+
...STOREFRONT_CSP_ALLOWLIST,
|
|
376
|
+
// Pin Astro's static inline hydration scripts + island style by hash
|
|
377
|
+
// (they're identical every request); my dynamic inline tags use the nonce.
|
|
378
|
+
scriptSrc: [
|
|
379
|
+
...STOREFRONT_CSP_ALLOWLIST.scriptSrc,
|
|
380
|
+
...STATIC_SCRIPT_HASHES,
|
|
381
|
+
...(foxyCsp?.scriptSrc ?? []),
|
|
382
|
+
],
|
|
383
|
+
styleSrc: [
|
|
384
|
+
...STOREFRONT_CSP_ALLOWLIST.styleSrc,
|
|
385
|
+
...STATIC_STYLE_HASHES,
|
|
386
|
+
...(foxyCsp?.styleSrc ?? []),
|
|
387
|
+
],
|
|
388
|
+
fontSrc: [...STOREFRONT_CSP_ALLOWLIST.fontSrc, ...(foxyCsp?.fontSrc ?? [])],
|
|
389
|
+
connectSrc: [
|
|
390
|
+
...STOREFRONT_CSP_ALLOWLIST.connectSrc,
|
|
391
|
+
...(foxyCsp?.connectSrc ?? []),
|
|
392
|
+
],
|
|
393
|
+
imgSrc: [...STOREFRONT_CSP_ALLOWLIST.imgSrc, ...(foxyCsp?.imgSrc ?? [])],
|
|
394
|
+
frameSrc: foxyCsp?.frameSrc ?? [],
|
|
395
|
+
formAction: foxyCsp?.formAction ?? [],
|
|
396
|
+
reportGroup: CSP_REPORT_GROUP,
|
|
397
|
+
reportUri: CSP_REPORT_PATH,
|
|
398
|
+
}),
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
// D1: store the fully-rendered, CSP-stamped response for the next reader. Only
|
|
402
|
+
// production (no preview token reached here) cacheable GETs have a pageKey;
|
|
403
|
+
// putCachedPage additionally skips non-200, non-HTML, and cookie-bearing
|
|
404
|
+
// responses. Also expose Cache-Control to any fronting CDN.
|
|
405
|
+
if (pageKey && cache && customization.env === "live") {
|
|
406
|
+
response.headers.set(
|
|
407
|
+
"Cache-Control",
|
|
408
|
+
"public, s-maxage=300, stale-while-revalidate=86400",
|
|
409
|
+
);
|
|
410
|
+
await putCachedPage(cache, pageKey, response);
|
|
411
|
+
}
|
|
412
|
+
return response;
|
|
413
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* 404 — written in the Northwind voice, not a stub. Offers a way home and a
|
|
4
|
+
* few collections to wander into so a dead link still lands somewhere useful.
|
|
5
|
+
*/
|
|
6
|
+
import Layout from "@/layouts/Layout.astro";
|
|
7
|
+
import CollectionCard from "@/components/CollectionCard.astro";
|
|
8
|
+
import { withBase } from "@/lib/basePath";
|
|
9
|
+
|
|
10
|
+
const { tenant, tot, basePath } = Astro.locals;
|
|
11
|
+
|
|
12
|
+
// A handful of collections to redirect lost visitors toward.
|
|
13
|
+
const collections = (await tot.listCollections()).slice(0, 3);
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<Layout
|
|
17
|
+
title="Page not found"
|
|
18
|
+
description="The page you were after isn't here."
|
|
19
|
+
path="/404"
|
|
20
|
+
noindex={true}
|
|
21
|
+
>
|
|
22
|
+
<section class="bg-[var(--color-bg)]">
|
|
23
|
+
<div class="container-prose py-[clamp(4rem,10vw,8rem)] text-center" data-reveal>
|
|
24
|
+
<p class="eyebrow mb-4">404</p>
|
|
25
|
+
<h1 class="display-xl mx-auto max-w-[18ch]">
|
|
26
|
+
This page wandered off in the fog.
|
|
27
|
+
</h1>
|
|
28
|
+
<p class="mx-auto mt-5 max-w-[44ch] text-[1.1rem] text-[var(--color-muted)]">
|
|
29
|
+
The link you followed leads nowhere we keep. No harm done — let's get you
|
|
30
|
+
back on the trail.
|
|
31
|
+
</p>
|
|
32
|
+
<div class="mt-9 flex flex-wrap items-center justify-center gap-3">
|
|
33
|
+
<a
|
|
34
|
+
href={withBase(basePath, "/")}
|
|
35
|
+
class="inline-flex items-center rounded-full bg-[var(--color-primary)] px-6 py-3 font-display text-sm font-medium text-[var(--color-primary-contrast)] transition-transform hover:-translate-y-0.5"
|
|
36
|
+
>
|
|
37
|
+
Back home
|
|
38
|
+
</a>
|
|
39
|
+
<a
|
|
40
|
+
href={withBase(basePath, "/collections")}
|
|
41
|
+
class="inline-flex items-center rounded-full border border-[var(--color-border)] px-6 py-3 font-display text-sm font-medium text-[var(--color-text)] transition-colors hover:border-[var(--color-primary)]"
|
|
42
|
+
>
|
|
43
|
+
Browse collections
|
|
44
|
+
</a>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</section>
|
|
48
|
+
|
|
49
|
+
{collections.length > 0 && (
|
|
50
|
+
<section class="bg-[var(--color-surface)] py-[clamp(2.5rem,6vw,4.5rem)]">
|
|
51
|
+
<div class="container-prose">
|
|
52
|
+
<p class="eyebrow mb-6 text-center">Maybe you were looking for</p>
|
|
53
|
+
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
|
54
|
+
{collections.map((c) => (
|
|
55
|
+
<CollectionCard collection={c} productCount={c.product_handles.length} />
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</section>
|
|
60
|
+
)}
|
|
61
|
+
</Layout>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Editorial pages from the ContentProvider (Storyblok seam; local fallback in
|
|
4
|
+
* Phase 1). Catch-all, lowest routing priority — Astro resolves /products,
|
|
5
|
+
* /collections, /search first. We still guard defensively against those
|
|
6
|
+
* prefixes so a stray slug can never shadow a real route.
|
|
7
|
+
*/
|
|
8
|
+
import Layout from "@/layouts/Layout.astro";
|
|
9
|
+
import Breadcrumbs from "@/components/Breadcrumbs.astro";
|
|
10
|
+
import { breadcrumbLd } from "@/lib/jsonld";
|
|
11
|
+
import { truncate } from "@/lib/format";
|
|
12
|
+
import { rawHtmlResponse } from "@/lib/rawMarketingHtml";
|
|
13
|
+
|
|
14
|
+
const { tenant, canonicalBase, content, basePath } = Astro.locals;
|
|
15
|
+
const slug = (Astro.params.slug ?? "").replace(/^\/+|\/+$/g, "");
|
|
16
|
+
|
|
17
|
+
// Defensive: never let the catch-all answer for first-class route namespaces.
|
|
18
|
+
// (Keeps the commerce boundary hard — a marketing subpage can never shadow a
|
|
19
|
+
// product/collection route.)
|
|
20
|
+
if (
|
|
21
|
+
!slug ||
|
|
22
|
+
slug.startsWith("products/") ||
|
|
23
|
+
slug.startsWith("collections/")
|
|
24
|
+
) {
|
|
25
|
+
return new Response(null, { status: 404 });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Preview/dev-only style-guide surfaces are requested at `/_style-guide/<tenant>/…`
|
|
29
|
+
// (the leading underscore signals internal). Astro won't route a `_`-prefixed pages
|
|
30
|
+
// dir, so the pages live under `/style-guide/…` and we rewrite the underscored URL
|
|
31
|
+
// onto them. These are noindex + sitemap-excluded and never reskin the live site.
|
|
32
|
+
if (slug === "_style-guide" || slug.startsWith("_style-guide/")) {
|
|
33
|
+
return Astro.rewrite("/style-guide/" + slug.slice("_style-guide".length + 1));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Raw tenant-authored marketing subpage (vendor-injected HTML) — serve the
|
|
37
|
+
// self-contained document VERBATIM, bypassing the block Layout, with the request
|
|
38
|
+
// CSP nonce stamped onto its first-party inline <style>/<script>. Same path as the
|
|
39
|
+
// raw home in index.astro. Middleware still applies canonical host, security
|
|
40
|
+
// headers, CSP, and (prod) the edge cache.
|
|
41
|
+
const rawHtml = await content.getPageHtml(slug);
|
|
42
|
+
if (rawHtml) {
|
|
43
|
+
// Shared chrome: a body-only fragment gets wrapped in the tenant's chrome.html;
|
|
44
|
+
// a full document serves verbatim (backward-compatible). See rawMarketingHtml.ts.
|
|
45
|
+
const chromeWrapper = await content.getChromeHtml();
|
|
46
|
+
return rawHtmlResponse(rawHtml, { nonce: Astro.locals.cspNonce, basePath, chromeWrapper });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const page = await content.getPage(slug);
|
|
50
|
+
if (!page) return new Response(null, { status: 404 });
|
|
51
|
+
|
|
52
|
+
const crumbs = [
|
|
53
|
+
{ label: tenant.displayName, href: "/" },
|
|
54
|
+
{ label: page.title, href: `/${page.slug}` },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const description =
|
|
58
|
+
page.seoDescription ||
|
|
59
|
+
(page.intro ? truncate(page.intro, 160) : undefined);
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
<Layout
|
|
63
|
+
title={page.seoTitle || page.title}
|
|
64
|
+
description={description}
|
|
65
|
+
path={`/${page.slug}`}
|
|
66
|
+
ogType="article"
|
|
67
|
+
jsonLd={breadcrumbLd(crumbs, canonicalBase)}
|
|
68
|
+
>
|
|
69
|
+
<div class="container-prose pt-6">
|
|
70
|
+
<Breadcrumbs crumbs={crumbs} />
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{/* Page hero */}
|
|
74
|
+
<header class="container-prose pb-2 pt-8">
|
|
75
|
+
{page.eyebrow && <p class="eyebrow eyebrow--tick mb-3">{page.eyebrow}</p>}
|
|
76
|
+
<h1 class="display-xl max-w-[20ch]">
|
|
77
|
+
{page.title}
|
|
78
|
+
</h1>
|
|
79
|
+
{page.intro && (
|
|
80
|
+
<p class="mt-5 max-w-[60ch] text-[1.1rem] text-[var(--color-muted)]">
|
|
81
|
+
{page.intro}
|
|
82
|
+
</p>
|
|
83
|
+
)}
|
|
84
|
+
</header>
|
|
85
|
+
|
|
86
|
+
{/* Body — constrained to a readable measure. */}
|
|
87
|
+
<div class="container-prose pb-[clamp(3rem,7vw,6rem)] pt-8">
|
|
88
|
+
<div class="rte mx-auto max-w-[68ch] text-[var(--color-text)]" set:html={page.body_html} />
|
|
89
|
+
</div>
|
|
90
|
+
</Layout>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/auth/logout — end the signed-in session.
|
|
3
|
+
*
|
|
4
|
+
* Revokes the server-side record (so the session can't be reused even if the
|
|
5
|
+
* cookie is replayed) and clears the cookie, then bounces to the login page.
|
|
6
|
+
* Exempt from the login gate (it's under /api/auth/), so a viewer can always
|
|
7
|
+
* sign out.
|
|
8
|
+
*/
|
|
9
|
+
import type { APIContext } from "astro";
|
|
10
|
+
import { getSessionStore, clearSessionCookie } from "@/lib/auth/route";
|
|
11
|
+
import { revokeSession, SESSION_COOKIE } from "@/lib/auth/session";
|
|
12
|
+
|
|
13
|
+
export const prerender = false;
|
|
14
|
+
|
|
15
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
16
|
+
const sid = context.cookies.get(SESSION_COOKIE)?.value ?? null;
|
|
17
|
+
const store = await getSessionStore();
|
|
18
|
+
if (store) await revokeSession(store, sid);
|
|
19
|
+
clearSessionCookie(context);
|
|
20
|
+
return context.redirect("/auth/login", 303);
|
|
21
|
+
}
|