@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,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic local placeholder art (brief: images note).
|
|
3
|
+
*
|
|
4
|
+
* Fixture image URLs are Shopify-CDN paths that 404 and we ship offline. Rather
|
|
5
|
+
* than a grey box, we synthesize a tasteful, aspect-correct SVG keyed by a
|
|
6
|
+
* stable seed (image id or product handle) so each product gets a consistent,
|
|
7
|
+
* on-brand gradient + monogram. The <Img> component swaps these out for real
|
|
8
|
+
* rehosted URLs when CatalogImage.rehosted === true.
|
|
9
|
+
*
|
|
10
|
+
* Palette is pulled from the theme's CSS vars at render time where possible, but
|
|
11
|
+
* the SVG is self-contained (inlines colors) so it works as a data URI / <img src>.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Quiet, on-brand duotone pairs (spruce/greige/amber family). Picked by seed.
|
|
15
|
+
const PAIRS: Array<[string, string]> = [
|
|
16
|
+
["#243B35", "#C9C2B2"], // spruce -> sand
|
|
17
|
+
["#33514A", "#E3DCCB"], // pine -> paper
|
|
18
|
+
["#1F3A34", "#B5612F"], // anchor -> amber
|
|
19
|
+
["#3A4A45", "#D8D1C2"], // slate-sage -> light sand
|
|
20
|
+
["#2C3E39", "#A88B5C"], // deep green -> ochre
|
|
21
|
+
["#1C2624", "#7E8B86"], // ink -> stone
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function hash(seed: string): number {
|
|
25
|
+
let h = 2166136261;
|
|
26
|
+
for (let i = 0; i < seed.length; i++) {
|
|
27
|
+
h ^= seed.charCodeAt(i);
|
|
28
|
+
h = Math.imul(h, 16777619);
|
|
29
|
+
}
|
|
30
|
+
return Math.abs(h);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PlaceholderOpts {
|
|
34
|
+
seed: string;
|
|
35
|
+
width: number;
|
|
36
|
+
height: number;
|
|
37
|
+
/** Short label drawn faintly (e.g. product initials). */
|
|
38
|
+
label?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Returns a complete SVG string (aspect-correct, deterministic). */
|
|
42
|
+
export function placeholderSvg({ seed, width, height, label }: PlaceholderOpts): string {
|
|
43
|
+
const h = hash(seed);
|
|
44
|
+
const pair = PAIRS[h % PAIRS.length] ?? PAIRS[0]!;
|
|
45
|
+
const [a, b] = pair;
|
|
46
|
+
const angle = (h % 6) * 30; // 0..150deg
|
|
47
|
+
const cx = 20 + (h % 60);
|
|
48
|
+
const cy = 20 + ((h >> 3) % 60);
|
|
49
|
+
const initials = (label ?? seed)
|
|
50
|
+
.split(/[\s-]+/)
|
|
51
|
+
.map((w) => w[0])
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.slice(0, 2)
|
|
54
|
+
.join("")
|
|
55
|
+
.toUpperCase();
|
|
56
|
+
|
|
57
|
+
// Two soft radial blooms over a linear base — reads as considered, not noisy.
|
|
58
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-hidden="true">
|
|
59
|
+
<defs>
|
|
60
|
+
<linearGradient id="g" gradientTransform="rotate(${angle})">
|
|
61
|
+
<stop offset="0%" stop-color="${a}"/>
|
|
62
|
+
<stop offset="100%" stop-color="${b}"/>
|
|
63
|
+
</linearGradient>
|
|
64
|
+
<radialGradient id="r" cx="${cx}%" cy="${cy}%" r="70%">
|
|
65
|
+
<stop offset="0%" stop-color="${b}" stop-opacity="0.55"/>
|
|
66
|
+
<stop offset="100%" stop-color="${a}" stop-opacity="0"/>
|
|
67
|
+
</radialGradient>
|
|
68
|
+
</defs>
|
|
69
|
+
<rect width="100%" height="100%" fill="url(#g)"/>
|
|
70
|
+
<rect width="100%" height="100%" fill="url(#r)"/>
|
|
71
|
+
<text x="50%" y="50%" dy="0.35em" text-anchor="middle"
|
|
72
|
+
font-family="'Space Grotesk', system-ui, sans-serif"
|
|
73
|
+
font-size="${Math.round(Math.min(width, height) * 0.16)}"
|
|
74
|
+
font-weight="600" letter-spacing="0.04em"
|
|
75
|
+
fill="${b}" fill-opacity="0.22">${initials || "•"}</text>
|
|
76
|
+
</svg>`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Base64 data URI form (for <img src> and OG images). */
|
|
80
|
+
export function placeholderDataUri(opts: PlaceholderOpts): string {
|
|
81
|
+
const svg = placeholderSvg(opts);
|
|
82
|
+
// btoa is available in Workers + browsers; encodeURIComponent path is the
|
|
83
|
+
// SSR-safe fallback that also handles non-latin labels.
|
|
84
|
+
const encoded = encodeURIComponent(svg)
|
|
85
|
+
.replace(/'/g, "%27")
|
|
86
|
+
.replace(/"/g, "%22");
|
|
87
|
+
return `data:image/svg+xml,${encoded}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** True if a CatalogImage URL is usable vs a dead/synthetic fixture path. */
|
|
91
|
+
export function isUsableImageUrl(url: string | undefined, rehosted?: boolean): boolean {
|
|
92
|
+
if (!url) return false;
|
|
93
|
+
if (rehosted) return true;
|
|
94
|
+
// The synthetic apparel fixtures use a fake shop id (…/s/files/1/0001/…) whose
|
|
95
|
+
// URLs 404 — draw placeholders for those. REAL store CDN URLs (e.g. a live POC
|
|
96
|
+
// migration) are usable as-is.
|
|
97
|
+
if (/\/s\/files\/1\/0001\//.test(url)) return false;
|
|
98
|
+
return /^https?:\/\//.test(url);
|
|
99
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-publish auto-refresh for the SHARED preview (reviewer/owner "it updates
|
|
3
|
+
* itself" experience). NOT the developer's local `tot dev` HMR loop — this is the
|
|
4
|
+
* shared `test`-env preview that others review.
|
|
5
|
+
*
|
|
6
|
+
* A tiny live-reload client is injected (by src/middleware) into `test`-env HTML
|
|
7
|
+
* pages ONLY — never `live`. It polls `GET /__preview/version?tenant=<id>` (the
|
|
8
|
+
* per-tenant edge cache-version integer, which bumps on every reconcile/publish;
|
|
9
|
+
* see lib/edgeCache.ts + api/preview/reconcile.ts) and reloads the page when the
|
|
10
|
+
* version changes. Poll-based v1; an SSE / Durable-Object PUSH is the later
|
|
11
|
+
* upgrade (TODO below) that removes the ~1s poll latency.
|
|
12
|
+
*
|
|
13
|
+
* CSP: the injected <script> carries the per-request nonce (same one the strict
|
|
14
|
+
* CSP header allows), so it is CSP-clean with no `unsafe-inline` and no inline
|
|
15
|
+
* event handlers.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const DEFAULT_POLL_MS = 1000;
|
|
19
|
+
|
|
20
|
+
/** Escape a value for safe embedding inside an inline <script> as JSON. */
|
|
21
|
+
function jsonForScript(value: unknown): string {
|
|
22
|
+
// JSON.stringify covers quoting; escaping `<` defends against a `</script>`
|
|
23
|
+
// (or `<!--`) breakout even though tenant ids / base paths never contain it.
|
|
24
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The live-reload client body (no <script> wrapper). Kept dependency-free and
|
|
29
|
+
* tiny so it is cheap to inline on every preview page.
|
|
30
|
+
*/
|
|
31
|
+
export function buildPreviewReloadScript(opts: {
|
|
32
|
+
tenantId: string;
|
|
33
|
+
basePath: string;
|
|
34
|
+
pollMs?: number;
|
|
35
|
+
}): string {
|
|
36
|
+
const tenant = jsonForScript(opts.tenantId);
|
|
37
|
+
const base = jsonForScript(opts.basePath ?? "");
|
|
38
|
+
const pollMs = Math.max(250, Math.floor(opts.pollMs ?? DEFAULT_POLL_MS));
|
|
39
|
+
// The version endpoint is fetched UNDER basePath so the same tenant-routing the
|
|
40
|
+
// page used resolves it (path-routed previews carry a /<domain> prefix).
|
|
41
|
+
return `(() => {
|
|
42
|
+
const TENANT = ${tenant}, BASE = ${base}, POLL_MS = ${pollMs};
|
|
43
|
+
const url = BASE + "/__preview/version?tenant=" + encodeURIComponent(TENANT);
|
|
44
|
+
let known = null;
|
|
45
|
+
const tick = async () => {
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch(url, { headers: { accept: "application/json" }, cache: "no-store" });
|
|
48
|
+
if (res.ok) {
|
|
49
|
+
const v = (await res.json()).version;
|
|
50
|
+
if (known === null) { known = v; }
|
|
51
|
+
// TODO(theme-hot-swap): when the change is theme-only, swap the CSS custom
|
|
52
|
+
// properties in place (no reload). Needs the reconcile to report a
|
|
53
|
+
// theme-only delta; gated behind this same version signal until then.
|
|
54
|
+
else if (v !== known) { location.reload(); return; }
|
|
55
|
+
}
|
|
56
|
+
} catch (_e) { /* transient — keep polling */ }
|
|
57
|
+
setTimeout(tick, POLL_MS);
|
|
58
|
+
};
|
|
59
|
+
// TODO(push): replace this poll with an SSE / Durable-Object stream so the
|
|
60
|
+
// reload is instant instead of within POLL_MS of a publish.
|
|
61
|
+
setTimeout(tick, POLL_MS);
|
|
62
|
+
})();`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** True when a response is HTML we can inject into. */
|
|
66
|
+
export function isHtmlResponse(response: Response): boolean {
|
|
67
|
+
if (response.status !== 200) return false;
|
|
68
|
+
return (response.headers.get("content-type") ?? "").includes("text/html");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Inject the live-reload <script> into an HTML string, before </body> (or
|
|
73
|
+
* appended when there is no </body>). Pure + unit-testable.
|
|
74
|
+
*/
|
|
75
|
+
export function injectPreviewReloadHtml(
|
|
76
|
+
html: string,
|
|
77
|
+
opts: { tenantId: string; basePath: string; nonce: string; pollMs?: number },
|
|
78
|
+
): string {
|
|
79
|
+
const script = `<script nonce="${opts.nonce}">${buildPreviewReloadScript(opts)}</script>`;
|
|
80
|
+
if (/<\/body>/i.test(html)) {
|
|
81
|
+
return html.replace(/<\/body>/i, `${script}</body>`);
|
|
82
|
+
}
|
|
83
|
+
return html + script;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Wrap a Response, returning a new one with the reload client injected. No-op
|
|
88
|
+
* (returns the original) for non-HTML responses. The caller must have already
|
|
89
|
+
* decided this is a `test`-env response — this helper does not check the env.
|
|
90
|
+
*/
|
|
91
|
+
export async function injectPreviewReload(
|
|
92
|
+
response: Response,
|
|
93
|
+
opts: { tenantId: string; basePath: string; nonce: string; pollMs?: number },
|
|
94
|
+
): Promise<Response> {
|
|
95
|
+
if (!isHtmlResponse(response)) return response;
|
|
96
|
+
const html = await response.text();
|
|
97
|
+
const injected = injectPreviewReloadHtml(html, opts);
|
|
98
|
+
// Preserve status + headers; the body length changed but we don't set a manual
|
|
99
|
+
// Content-Length anywhere (Workers/Astro stream), so no header fix-up needed.
|
|
100
|
+
return new Response(injected, {
|
|
101
|
+
status: response.status,
|
|
102
|
+
statusText: response.statusText,
|
|
103
|
+
headers: response.headers,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Env-gated entrypoint the middleware calls: inject the reload client ONLY for
|
|
109
|
+
* the SHARED preview (`test`) — NEVER `live`. Returns the response unchanged for
|
|
110
|
+
* `live` (or any non-HTML response). This is the single guardrail seam, so the
|
|
111
|
+
* "present in test, absent in live" contract is unit-testable here.
|
|
112
|
+
*/
|
|
113
|
+
export async function maybeInjectPreviewReload(
|
|
114
|
+
response: Response,
|
|
115
|
+
opts: {
|
|
116
|
+
env: "live" | "test";
|
|
117
|
+
tenantId: string;
|
|
118
|
+
basePath: string;
|
|
119
|
+
nonce: string;
|
|
120
|
+
pollMs?: number;
|
|
121
|
+
},
|
|
122
|
+
): Promise<Response> {
|
|
123
|
+
if (opts.env !== "test") return response;
|
|
124
|
+
return injectPreviewReload(response, opts);
|
|
125
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw marketing-HTML serving — the "vendor injects their own HTML" path.
|
|
3
|
+
*
|
|
4
|
+
* A marketing tenant can ship a self-contained document (`content/<tenant>/home.html`
|
|
5
|
+
* or `content/<tenant>/pages-html/<slug>.html`) that we serve VERBATIM, bypassing the
|
|
6
|
+
* block Layout. Two transforms are applied at serve time:
|
|
7
|
+
* 1. nonce-stamp the document's inline <style>/<script> so a strict CSP admits them;
|
|
8
|
+
* 2. under path-prefix tenant routing (locals.basePath set), prefix same-origin
|
|
9
|
+
* root-absolute anchor hrefs with the base path so navigation stays in-tenant —
|
|
10
|
+
* the raw doc bypasses the Layout's client base-path fixer, so we do it here.
|
|
11
|
+
*
|
|
12
|
+
* Single home for both routes (`index.astro`, `[...slug].astro`).
|
|
13
|
+
*
|
|
14
|
+
* SECURITY: this trusts the tenant's HTML (first-party marketing). The nonce
|
|
15
|
+
* injection is a deliberate string replace — robust for first-party documents, not a
|
|
16
|
+
* sanitizer. Untrusted vendor HTML must be sandboxed/sanitized instead.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Marker a body-only page fragment's chrome wrapper uses to say "the page body
|
|
21
|
+
* goes here". A tenant's `content/<tenant>/chrome.html` is a full document with
|
|
22
|
+
* this comment where the fragment is spliced in. Kept as an HTML comment so the
|
|
23
|
+
* wrapper is still valid, previewable HTML on its own.
|
|
24
|
+
*/
|
|
25
|
+
export const CHROME_BODY_MARKER = "<!--PAGE_BODY-->";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Marker in the chrome wrapper's <head> where a fragment's page-specific <head>
|
|
29
|
+
* (title / meta / canonical / OG / JSON-LD) is spliced in — so shared chrome does
|
|
30
|
+
* NOT flatten per-page SEO. A fragment may lead with a `<head>…</head>` block; its
|
|
31
|
+
* contents go here, the rest goes to CHROME_BODY_MARKER.
|
|
32
|
+
*/
|
|
33
|
+
export const CHROME_HEAD_MARKER = "<!--PAGE_HEAD-->";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* True when `html` is a complete document (has <html> or a doctype) rather than a
|
|
37
|
+
* body-only fragment. Full documents are served verbatim; fragments get wrapped
|
|
38
|
+
* in the tenant's shared chrome. This is what keeps the mechanism backward-
|
|
39
|
+
* compatible — every existing raw home/subpage is a full document.
|
|
40
|
+
*/
|
|
41
|
+
export function isFullDocument(html: string): boolean {
|
|
42
|
+
return /^\s*<!doctype/i.test(html) || /<html[\s>]/i.test(html);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Splice a body-only fragment into a tenant chrome wrapper at CHROME_BODY_MARKER.
|
|
47
|
+
* If the wrapper has no marker (misconfigured), the fragment is returned unwrapped
|
|
48
|
+
* so a page never disappears.
|
|
49
|
+
*/
|
|
50
|
+
export function wrapFragmentInChrome(fragment: string, chromeWrapper: string): string {
|
|
51
|
+
if (!chromeWrapper.includes(CHROME_BODY_MARKER)) return fragment;
|
|
52
|
+
// A fragment may lead with a <head>…</head> of page-specific SEO tags. Split it
|
|
53
|
+
// out so the shared wrapper keeps per-page title/meta/canonical/JSON-LD.
|
|
54
|
+
let head = "";
|
|
55
|
+
let body = fragment;
|
|
56
|
+
const m = fragment.match(/^\s*<head[^>]*>([\s\S]*?)<\/head>/i);
|
|
57
|
+
if (m) {
|
|
58
|
+
head = m[1] ?? "";
|
|
59
|
+
body = fragment.slice(m[0].length);
|
|
60
|
+
}
|
|
61
|
+
let out = chromeWrapper.replace(CHROME_BODY_MARKER, body);
|
|
62
|
+
if (out.includes(CHROME_HEAD_MARKER)) out = out.replace(CHROME_HEAD_MARKER, head);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Stamp `nonce` onto every inline `<style>`/`<script>` so a strict CSP admits them. */
|
|
67
|
+
export function nonceInlineTags(html: string, nonce: string): string {
|
|
68
|
+
return html
|
|
69
|
+
.replaceAll("<style>", `<style nonce="${nonce}">`)
|
|
70
|
+
.replaceAll("<script", `<script nonce="${nonce}"`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Prefix same-origin root-absolute hrefs (`href="/…"`) with the tenant base path,
|
|
75
|
+
* so `/pricing/` becomes `/<domain>/pricing/` under path-prefix routing. No-op when
|
|
76
|
+
* basePath is empty (host-routed prod). Skips protocol-relative `//…` and, crucially,
|
|
77
|
+
* `/tenants/…` — those are GLOBAL static assets (logo, favicon) served at root under
|
|
78
|
+
* both routing modes, so they must NOT get the tenant prefix. Asset `src="…"` isn't
|
|
79
|
+
* matched at all (href-only).
|
|
80
|
+
*/
|
|
81
|
+
export function prefixTenantLinks(html: string, basePath: string): string {
|
|
82
|
+
if (!basePath) return html;
|
|
83
|
+
return html.replace(/href="\/(?!\/|tenants\/)/g, `href="${basePath}/`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build the HTML Response for a raw marketing page (nonce + base-path).
|
|
88
|
+
*
|
|
89
|
+
* Shared-chrome mechanism: a page is served in one of two ways —
|
|
90
|
+
* - FULL DOCUMENT (has <html>/doctype): served verbatim, as before.
|
|
91
|
+
* - BODY FRAGMENT + a tenant `chromeWrapper`: the fragment is spliced into the
|
|
92
|
+
* wrapper's CHROME_BODY_MARKER so chrome lives in ONE place, not copied into
|
|
93
|
+
* every page. A fragment with no wrapper still serves (unwrapped) for safety.
|
|
94
|
+
* Either way, the FINAL document gets nonce-stamped and base-path-prefixed, so the
|
|
95
|
+
* chrome's own inline <style>/<script> and root-absolute nav links are handled too.
|
|
96
|
+
*/
|
|
97
|
+
export function rawHtmlResponse(
|
|
98
|
+
html: string,
|
|
99
|
+
opts: { nonce: string; basePath: string; chromeWrapper?: string | null },
|
|
100
|
+
): Response {
|
|
101
|
+
let composed =
|
|
102
|
+
isFullDocument(html) || !opts.chromeWrapper
|
|
103
|
+
? html
|
|
104
|
+
: wrapFragmentInChrome(html, opts.chromeWrapper);
|
|
105
|
+
// DEV ONLY: these pages are served verbatim, bypassing Astro's render pipeline,
|
|
106
|
+
// so the Vite HMR client is never injected — a save would reach the server (see
|
|
107
|
+
// the tenantHotReload plugin in astro.config.mjs) but the browser would have no
|
|
108
|
+
// listener to act on the full-reload. Inject the client so save→reload works.
|
|
109
|
+
// Stamped with the page nonce by nonceInlineTags below; tree-shaken from prod.
|
|
110
|
+
if (import.meta.env.DEV) {
|
|
111
|
+
const client = `<script type="module" src="/@vite/client"></script>`;
|
|
112
|
+
composed = composed.includes("</head>")
|
|
113
|
+
? composed.replace("</head>", `${client}</head>`)
|
|
114
|
+
: `${client}${composed}`;
|
|
115
|
+
// Inline-AI edit loop's floating "✦ ask" affordance (WS5 G1) — same reason
|
|
116
|
+
// as the HMR client above, these pages bypass the Astro pipeline that
|
|
117
|
+
// inlineAiEditPlugin's transformIndexHtml hook (astro.config.mjs) covers
|
|
118
|
+
// for everything else, so it's injected here too. Unconditional in dev,
|
|
119
|
+
// matching the Vite client above: whether `tot dev` set an active tenant
|
|
120
|
+
// (TOT_DEV_TENANT) is a Node-process env var, invisible to this SSR code
|
|
121
|
+
// when it runs inside the Cloudflare/workerd dev runtime (see tot-dev.mjs's
|
|
122
|
+
// own note on this exact gap) — so the gate lives server-side instead, in
|
|
123
|
+
// inlineAiEditPlugin's Vite middleware, which DOES see the real env.
|
|
124
|
+
const widget = `<script type="module" src="/__tot/ai-widget-client.js"></script>`;
|
|
125
|
+
composed = composed.includes("</body>")
|
|
126
|
+
? composed.replace("</body>", `${widget}</body>`)
|
|
127
|
+
: `${composed}${widget}`;
|
|
128
|
+
}
|
|
129
|
+
const doc = prefixTenantLinks(nonceInlineTags(composed, opts.nonce), opts.basePath);
|
|
130
|
+
return new Response(doc, {
|
|
131
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory Fuse search index — the OFFLINE search floor.
|
|
3
|
+
*
|
|
4
|
+
* Used by FixtureToTClient.search (no D1 binding): typo-tolerant, relevance-ranked
|
|
5
|
+
* search over the bundled catalog snapshot, zero infra. When a D1 binding is wired
|
|
6
|
+
* (D1ToTClient), search instead goes through FTS5 — see lib/d1/catalog.ts and
|
|
7
|
+
* epics/edge-read-replica.md. This module is the portable floor an extracted
|
|
8
|
+
* merchant gets out of the box (docs/extraction-runbook.md).
|
|
9
|
+
*/
|
|
10
|
+
import Fuse, { type IFuseOptions } from "fuse.js";
|
|
11
|
+
import type { CatalogProduct } from "@tot/public-runtime";
|
|
12
|
+
|
|
13
|
+
export interface SearchHit {
|
|
14
|
+
product: CatalogProduct;
|
|
15
|
+
score: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SearchIndex {
|
|
19
|
+
search(query: string, limit?: number): SearchHit[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const FUSE_OPTIONS: IFuseOptions<CatalogProduct> = {
|
|
23
|
+
includeScore: true,
|
|
24
|
+
threshold: 0.38, // typo tolerance: lower = stricter
|
|
25
|
+
ignoreLocation: true,
|
|
26
|
+
minMatchCharLength: 2,
|
|
27
|
+
keys: [
|
|
28
|
+
{ name: "title", weight: 0.5 },
|
|
29
|
+
{ name: "vendor", weight: 0.15 },
|
|
30
|
+
{ name: "product_type", weight: 0.15 },
|
|
31
|
+
{ name: "tags", weight: 0.12 },
|
|
32
|
+
{ name: "description_text", weight: 0.08 },
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function buildSearchIndex(products: CatalogProduct[]): SearchIndex {
|
|
37
|
+
// Defensive: only ever index active products (the draft canary must never leak).
|
|
38
|
+
const active = products.filter((p) => p.status === "active");
|
|
39
|
+
const fuse = new Fuse(active, FUSE_OPTIONS);
|
|
40
|
+
return {
|
|
41
|
+
search(query: string, limit = 12): SearchHit[] {
|
|
42
|
+
const q = query.trim();
|
|
43
|
+
if (!q) return [];
|
|
44
|
+
return fuse.search(q, { limit }).map((r) => ({ product: r.item, score: r.score ?? 1 }));
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Compact shape sent to the typeahead island (keeps client JSON tiny). */
|
|
50
|
+
export interface TypeaheadDoc {
|
|
51
|
+
handle: string;
|
|
52
|
+
title: string;
|
|
53
|
+
vendor?: string;
|
|
54
|
+
type?: string;
|
|
55
|
+
price: number;
|
|
56
|
+
currency: string;
|
|
57
|
+
image?: string;
|
|
58
|
+
tags: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function toTypeaheadDoc(p: CatalogProduct): TypeaheadDoc {
|
|
62
|
+
const doc: TypeaheadDoc = {
|
|
63
|
+
handle: p.handle,
|
|
64
|
+
title: p.title,
|
|
65
|
+
price: p.price_range.min,
|
|
66
|
+
currency: p.price_range.currency,
|
|
67
|
+
tags: p.tags,
|
|
68
|
+
};
|
|
69
|
+
if (p.vendor) doc.vendor = p.vendor;
|
|
70
|
+
if (p.product_type) doc.type = p.product_type;
|
|
71
|
+
const img = p.images[0];
|
|
72
|
+
if (img?.url) doc.image = img.url;
|
|
73
|
+
return doc;
|
|
74
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SectionSource — the single, typed contract every merchandising section binds
|
|
3
|
+
* to. A section component (ProductCarousel, ProductGrid, …) never talks to the
|
|
4
|
+
* catalog directly; it declares WHAT set of products it wants as a SectionSource
|
|
5
|
+
* and calls resolveSectionSource() to get them. This keeps the components
|
|
6
|
+
* tenant-agnostic and data-source-agnostic: swap the ToTClient backend (D1 vs
|
|
7
|
+
* fixture) and every section keeps working.
|
|
8
|
+
*
|
|
9
|
+
* Fidelity to a merchant's storefront comes from theme tokens + variant props +
|
|
10
|
+
* the source they point a section at — NEVER from forking a component per
|
|
11
|
+
* merchant. That's what keeps a tenant single-tenant-extractable.
|
|
12
|
+
*/
|
|
13
|
+
import type { CatalogProduct } from "@tot/public-runtime";
|
|
14
|
+
import type { ToTClient } from "@/lib/tot/ToTClient";
|
|
15
|
+
import { isOnSale } from "@/lib/tot/query";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Where a merchandising section gets its products. Exactly one shape per source.
|
|
19
|
+
* `handle` placeholders are product/collection handles (unique per tenant).
|
|
20
|
+
*/
|
|
21
|
+
export type SectionSource =
|
|
22
|
+
| { collection: string } // products in a collection (by handle)
|
|
23
|
+
| { tag: string } // products carrying a tag
|
|
24
|
+
| { featured: true } // the curated/featured set (getFeatured)
|
|
25
|
+
| { newest: true } // newest-first
|
|
26
|
+
| { bestSelling: true } // bestseller-tagged
|
|
27
|
+
| { onSale: true } // anything with a live compare-at markdown
|
|
28
|
+
| { handles: string[] } // an explicit, ordered handle list
|
|
29
|
+
| { related: string } // products related to a base product (by handle)
|
|
30
|
+
| { recentlyViewed: true }; // client-personalized — resolves empty server-side
|
|
31
|
+
|
|
32
|
+
export const DEFAULT_SECTION_LIMIT = 12;
|
|
33
|
+
|
|
34
|
+
/** Bestseller signal is tag-based in Phase 1 (no order history at the edge). */
|
|
35
|
+
const BESTSELLER_TAGS = new Set(["bestseller", "best-seller", "best-sellers"]);
|
|
36
|
+
|
|
37
|
+
const isActive = (p: CatalogProduct) => p.status === "active";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a SectionSource to a concrete, active product list via the tenant's
|
|
41
|
+
* ToTClient. Always returns at most `limit` ACTIVE products (drafts/archived are
|
|
42
|
+
* excluded from every public surface — see ToTClient docs). Backend-agnostic:
|
|
43
|
+
* tag/onSale/bestSelling filter over the client's list methods rather than
|
|
44
|
+
* assuming a SQL facet, so the fixture and D1 backends behave identically.
|
|
45
|
+
*/
|
|
46
|
+
export async function resolveSectionSource(
|
|
47
|
+
source: SectionSource,
|
|
48
|
+
tot: ToTClient,
|
|
49
|
+
limit: number = DEFAULT_SECTION_LIMIT,
|
|
50
|
+
): Promise<CatalogProduct[]> {
|
|
51
|
+
if ("collection" in source) {
|
|
52
|
+
const { products } = await tot.listProducts({
|
|
53
|
+
collection: source.collection,
|
|
54
|
+
pageSize: limit,
|
|
55
|
+
});
|
|
56
|
+
return products.slice(0, limit);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if ("featured" in source) {
|
|
60
|
+
return (await tot.getFeatured(limit)).slice(0, limit);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if ("newest" in source) {
|
|
64
|
+
const { products } = await tot.listProducts({ sort: "newest", pageSize: limit });
|
|
65
|
+
return products.slice(0, limit);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if ("handles" in source) {
|
|
69
|
+
const found = await Promise.all(
|
|
70
|
+
source.handles.map((h) => tot.getProductByHandle(h)),
|
|
71
|
+
);
|
|
72
|
+
return found
|
|
73
|
+
.filter((p): p is CatalogProduct => !!p && isActive(p))
|
|
74
|
+
.slice(0, limit);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if ("tag" in source) {
|
|
78
|
+
const all = await tot.getAllActiveProducts();
|
|
79
|
+
return all.filter((p) => p.tags.includes(source.tag)).slice(0, limit);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if ("onSale" in source) {
|
|
83
|
+
const all = await tot.getAllActiveProducts();
|
|
84
|
+
return all.filter(isOnSale).slice(0, limit);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if ("bestSelling" in source) {
|
|
88
|
+
const all = await tot.getAllActiveProducts();
|
|
89
|
+
return all
|
|
90
|
+
.filter((p) => p.tags.some((t) => BESTSELLER_TAGS.has(t)))
|
|
91
|
+
.slice(0, limit);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if ("related" in source) {
|
|
95
|
+
const base = await tot.getProductByHandle(source.related);
|
|
96
|
+
if (!base) return [];
|
|
97
|
+
const baseCollections = new Set(base.collections);
|
|
98
|
+
const baseTags = new Set(base.tags);
|
|
99
|
+
const all = await tot.getAllActiveProducts();
|
|
100
|
+
const scored = all
|
|
101
|
+
.filter((p) => p.handle !== base.handle)
|
|
102
|
+
.map((p) => {
|
|
103
|
+
const shared =
|
|
104
|
+
p.collections.filter((c) => baseCollections.has(c)).length +
|
|
105
|
+
p.tags.filter((t) => baseTags.has(t)).length;
|
|
106
|
+
return { p, shared };
|
|
107
|
+
})
|
|
108
|
+
.filter((s) => s.shared > 0)
|
|
109
|
+
.sort((a, b) => b.shared - a.shared);
|
|
110
|
+
return scored.slice(0, limit).map((s) => s.p);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// recentlyViewed: server has no per-visitor history; the client island
|
|
114
|
+
// (RecentlyViewed.tsx) hydrates this. Resolve empty so the section can decide
|
|
115
|
+
// to hide or defer to its island.
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Ready-made sources for the common merchandising rows. A preset carousel is
|
|
121
|
+
* just `<ProductCarousel source={SECTION_PRESETS.bestSellers} />` — there are no
|
|
122
|
+
* separate BestSellers/NewArrivals/OnSale components to maintain.
|
|
123
|
+
*/
|
|
124
|
+
export const SECTION_PRESETS = {
|
|
125
|
+
featured: { featured: true } as SectionSource,
|
|
126
|
+
bestSellers: { bestSelling: true } as SectionSource,
|
|
127
|
+
newArrivals: { newest: true } as SectionSource,
|
|
128
|
+
onSale: { onSale: true } as SectionSource,
|
|
129
|
+
} as const;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baseline security response headers (Epic Phase-2 S1).
|
|
3
|
+
*
|
|
4
|
+
* Applied to every storefront response in middleware and baked into cached pages.
|
|
5
|
+
* The Content-Security-Policy itself (default-src/frame-ancestors/base-uri/…) is
|
|
6
|
+
* built separately in @tot/private-controlplane `csp.ts`; these are the complementary
|
|
7
|
+
* transport / sniffing / referrer / feature-permission headers that the appraisal
|
|
8
|
+
* flagged as missing.
|
|
9
|
+
*/
|
|
10
|
+
export const SECURITY_HEADERS: Readonly<Record<string, string>> = {
|
|
11
|
+
// 2-year HSTS with preload — production is HTTPS-only behind Cloudflare;
|
|
12
|
+
// browsers ignore this header on plain http (localhost dev), so it's safe to
|
|
13
|
+
// always set.
|
|
14
|
+
"Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload",
|
|
15
|
+
"X-Content-Type-Options": "nosniff",
|
|
16
|
+
// Legacy backstop; modern browsers honour the CSP `frame-ancestors 'self'`.
|
|
17
|
+
"X-Frame-Options": "SAMEORIGIN",
|
|
18
|
+
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
19
|
+
// Lock powerful features off for the browse storefront. Verification and
|
|
20
|
+
// checkout (Powered by FoxyCart) run in their own surfaces that grant only
|
|
21
|
+
// what they need via per-frame Permissions-Policy.
|
|
22
|
+
"Permissions-Policy":
|
|
23
|
+
"camera=(), microphone=(), geolocation=(), browsing-topics=(), interest-cohort=()",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Set the baseline headers on a response, without clobbering anything already set. */
|
|
27
|
+
export function applySecurityHeaders(headers: Headers): void {
|
|
28
|
+
for (const [key, value] of Object.entries(SECURITY_HEADERS)) {
|
|
29
|
+
if (!headers.has(key)) headers.set(key, value);
|
|
30
|
+
}
|
|
31
|
+
}
|