@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,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D1 (edge SQLite) catalog read-model — schema + row projections (the one-store
|
|
3
|
+
* target from epics/edge-read-replica.md). The storefront reads search, faceted
|
|
4
|
+
* listing, and full products from D1; the ETL emits the rows. This is the shared
|
|
5
|
+
* contract both sides agree on.
|
|
6
|
+
*
|
|
7
|
+
* - `products` — one row per product: slim query columns + a `data` blob
|
|
8
|
+
* (the full CatalogProduct JSON) for PDP/listing retrieval.
|
|
9
|
+
* - `product_collections` — membership (handle -> collection handle).
|
|
10
|
+
* - `product_facets` — (handle, name, value) for SQL GROUP BY facet counts.
|
|
11
|
+
* - `products_fts` — FTS5 (trigram) over the searchable text: typo-tolerant
|
|
12
|
+
* search without loading the catalog into the Worker.
|
|
13
|
+
*/
|
|
14
|
+
import type { CatalogCollection, CatalogProduct } from "./product.js";
|
|
15
|
+
|
|
16
|
+
export const CATALOG_D1_SCHEMA = `
|
|
17
|
+
CREATE TABLE IF NOT EXISTS products (
|
|
18
|
+
handle TEXT PRIMARY KEY NOT NULL,
|
|
19
|
+
title TEXT NOT NULL,
|
|
20
|
+
vendor TEXT,
|
|
21
|
+
product_type TEXT,
|
|
22
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
23
|
+
description_text TEXT NOT NULL DEFAULT '',
|
|
24
|
+
price_min REAL NOT NULL,
|
|
25
|
+
price_max REAL NOT NULL,
|
|
26
|
+
currency TEXT NOT NULL,
|
|
27
|
+
image TEXT,
|
|
28
|
+
available INTEGER NOT NULL DEFAULT 0,
|
|
29
|
+
status TEXT NOT NULL,
|
|
30
|
+
position INTEGER NOT NULL DEFAULT 0,
|
|
31
|
+
created_at TEXT,
|
|
32
|
+
data TEXT NOT NULL
|
|
33
|
+
);
|
|
34
|
+
CREATE TABLE IF NOT EXISTS product_collections (
|
|
35
|
+
handle TEXT NOT NULL,
|
|
36
|
+
collection TEXT NOT NULL
|
|
37
|
+
);
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_pc_collection ON product_collections (collection);
|
|
39
|
+
CREATE TABLE IF NOT EXISTS product_facets (
|
|
40
|
+
handle TEXT NOT NULL,
|
|
41
|
+
name TEXT NOT NULL,
|
|
42
|
+
value TEXT NOT NULL
|
|
43
|
+
);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_pf_name ON product_facets (name, value);
|
|
45
|
+
CREATE TABLE IF NOT EXISTS collections (
|
|
46
|
+
handle TEXT PRIMARY KEY NOT NULL,
|
|
47
|
+
title TEXT NOT NULL,
|
|
48
|
+
position INTEGER NOT NULL DEFAULT 99,
|
|
49
|
+
data TEXT NOT NULL
|
|
50
|
+
);
|
|
51
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS products_fts USING fts5 (
|
|
52
|
+
handle UNINDEXED, title, vendor, product_type, tags, description_text,
|
|
53
|
+
tokenize = 'trigram'
|
|
54
|
+
);
|
|
55
|
+
`.trim();
|
|
56
|
+
|
|
57
|
+
export interface ProductRow {
|
|
58
|
+
handle: string;
|
|
59
|
+
title: string;
|
|
60
|
+
vendor: string | null;
|
|
61
|
+
product_type: string | null;
|
|
62
|
+
tags: string; // JSON array
|
|
63
|
+
description_text: string;
|
|
64
|
+
price_min: number;
|
|
65
|
+
price_max: number;
|
|
66
|
+
currency: string;
|
|
67
|
+
image: string | null;
|
|
68
|
+
available: number; // 0 | 1
|
|
69
|
+
status: string;
|
|
70
|
+
position: number;
|
|
71
|
+
created_at: string | null;
|
|
72
|
+
data: string; // full CatalogProduct JSON
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const firstImage = (p: CatalogProduct): string | null =>
|
|
76
|
+
(p.images.find((i) => i.url) ?? p.images[0])?.url ?? null;
|
|
77
|
+
|
|
78
|
+
/** Project a CatalogProduct into the `products` row. */
|
|
79
|
+
export function toProductRow(p: CatalogProduct, position = 0): ProductRow {
|
|
80
|
+
return {
|
|
81
|
+
handle: p.handle,
|
|
82
|
+
title: p.title,
|
|
83
|
+
vendor: p.vendor ?? null,
|
|
84
|
+
product_type: p.product_type ?? null,
|
|
85
|
+
tags: JSON.stringify(p.tags),
|
|
86
|
+
description_text: p.description_text,
|
|
87
|
+
price_min: p.price_range.min,
|
|
88
|
+
price_max: p.price_range.max,
|
|
89
|
+
currency: p.price_range.currency,
|
|
90
|
+
image: firstImage(p),
|
|
91
|
+
available: p.variants.some((v) => v.inventory.available) ? 1 : 0,
|
|
92
|
+
status: p.status,
|
|
93
|
+
position,
|
|
94
|
+
created_at: p.created_at,
|
|
95
|
+
data: JSON.stringify(p),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Facet rows: one per distinct option value (across variants) + one per tag. */
|
|
100
|
+
export function facetRows(p: CatalogProduct): Array<{ name: string; value: string }> {
|
|
101
|
+
const out: Array<{ name: string; value: string }> = [];
|
|
102
|
+
for (const opt of p.options) {
|
|
103
|
+
const seen = new Set<string>();
|
|
104
|
+
for (const v of p.variants) {
|
|
105
|
+
const val = v.option_values[opt.name];
|
|
106
|
+
if (val && !seen.has(val)) {
|
|
107
|
+
seen.add(val);
|
|
108
|
+
out.push({ name: opt.name, value: val });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const tag of p.tags) out.push({ name: "tags", value: tag });
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Collection-membership rows for a product. */
|
|
117
|
+
export function collectionRows(p: CatalogProduct): Array<{ collection: string }> {
|
|
118
|
+
return p.collections.map((collection) => ({ collection }));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface CollectionRow {
|
|
122
|
+
handle: string;
|
|
123
|
+
title: string;
|
|
124
|
+
position: number;
|
|
125
|
+
data: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Project a CatalogCollection into the `collections` row. */
|
|
129
|
+
export function toCollectionRow(c: CatalogCollection): CollectionRow {
|
|
130
|
+
return {
|
|
131
|
+
handle: c.handle,
|
|
132
|
+
title: c.title,
|
|
133
|
+
position: c.position ?? 99,
|
|
134
|
+
data: JSON.stringify(c),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** SQL literal: NULL, a number, or a single-quote-escaped string. */
|
|
139
|
+
function lit(v: string | number | null): string {
|
|
140
|
+
if (v === null) return "NULL";
|
|
141
|
+
if (typeof v === "number") return Number.isFinite(v) ? String(v) : "NULL";
|
|
142
|
+
return `'${v.replace(/'/g, "''")}'`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Emit a self-contained SQL seed (schema + a clean reseed of all rows) for a
|
|
147
|
+
* tenant, loadable into D1 via `wrangler d1 execute <DB> --file catalog.sql`.
|
|
148
|
+
* Includes ALL products (status column drives query visibility; getProductByHandle
|
|
149
|
+
* still resolves drafts). Re-runnable: it clears the tables before inserting.
|
|
150
|
+
*/
|
|
151
|
+
export function emitCatalogSql(
|
|
152
|
+
products: CatalogProduct[],
|
|
153
|
+
collections: CatalogCollection[] = [],
|
|
154
|
+
): string {
|
|
155
|
+
const out: string[] = [
|
|
156
|
+
CATALOG_D1_SCHEMA,
|
|
157
|
+
"DELETE FROM products;",
|
|
158
|
+
"DELETE FROM product_collections;",
|
|
159
|
+
"DELETE FROM product_facets;",
|
|
160
|
+
"DELETE FROM products_fts;",
|
|
161
|
+
"DELETE FROM collections;",
|
|
162
|
+
];
|
|
163
|
+
for (const c of collections) {
|
|
164
|
+
const r = toCollectionRow(c);
|
|
165
|
+
out.push(
|
|
166
|
+
`INSERT INTO collections (handle,title,position,data) VALUES (${[r.handle, r.title, r.position, r.data].map(lit).join(",")});`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
let position = 0;
|
|
170
|
+
for (const p of products) {
|
|
171
|
+
const r = toProductRow(p, position++);
|
|
172
|
+
out.push(
|
|
173
|
+
`INSERT INTO products (handle,title,vendor,product_type,tags,description_text,price_min,price_max,currency,image,available,status,position,created_at,data) VALUES (${[
|
|
174
|
+
r.handle, r.title, r.vendor, r.product_type, r.tags, r.description_text,
|
|
175
|
+
r.price_min, r.price_max, r.currency, r.image, r.available, r.status,
|
|
176
|
+
r.position, r.created_at, r.data,
|
|
177
|
+
].map(lit).join(",")});`,
|
|
178
|
+
);
|
|
179
|
+
out.push(
|
|
180
|
+
`INSERT INTO products_fts (handle,title,vendor,product_type,tags,description_text) VALUES (${[
|
|
181
|
+
p.handle, p.title, p.vendor ?? "", p.product_type ?? "", p.tags.join(" "), p.description_text,
|
|
182
|
+
].map(lit).join(",")});`,
|
|
183
|
+
);
|
|
184
|
+
for (const f of facetRows(p)) {
|
|
185
|
+
out.push(`INSERT INTO product_facets (handle,name,value) VALUES (${lit(p.handle)},${lit(f.name)},${lit(f.value)});`);
|
|
186
|
+
}
|
|
187
|
+
for (const c of collectionRows(p)) {
|
|
188
|
+
out.push(`INSERT INTO product_collections (handle,collection) VALUES (${lit(p.handle)},${lit(c.collection)});`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return `${out.join("\n")}\n`;
|
|
192
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-Security-Policy builder. Pure + dep-free so it's unit-testable and
|
|
3
|
+
* reusable across the storefront.
|
|
4
|
+
*
|
|
5
|
+
* Produces a strict policy: `default-src 'self'`, no `unsafe-inline`. The
|
|
6
|
+
* platform's own inline `<style>`/`<script is:inline>` are allowed via a
|
|
7
|
+
* per-request nonce; everything else must be `'self'`, an explicitly allowlisted
|
|
8
|
+
* host, or (for tenant scripts) a per-version hash passed in `scriptSrc`.
|
|
9
|
+
*
|
|
10
|
+
* The middleware decides the header name — `Content-Security-Policy` (enforce)
|
|
11
|
+
* vs `Content-Security-Policy-Report-Only` (rollout). This only builds the value.
|
|
12
|
+
*/
|
|
13
|
+
export interface CspOptions {
|
|
14
|
+
/** Per-request nonce; the same value goes on the platform's inline tags. */
|
|
15
|
+
nonce: string;
|
|
16
|
+
/** Extra script-src entries: allowlisted CDNs + per-version tenant hashes. */
|
|
17
|
+
scriptSrc?: readonly string[];
|
|
18
|
+
styleSrc?: readonly string[];
|
|
19
|
+
fontSrc?: readonly string[];
|
|
20
|
+
imgSrc?: readonly string[];
|
|
21
|
+
connectSrc?: readonly string[];
|
|
22
|
+
/** Sandboxed-widget origins. */
|
|
23
|
+
frameSrc?: readonly string[];
|
|
24
|
+
/**
|
|
25
|
+
* Extra `form-action` targets. Default is `'self'`; the FoxyCart add-to-cart
|
|
26
|
+
* form POSTs cross-origin to the store's `/cart`, so that origin is added here.
|
|
27
|
+
*/
|
|
28
|
+
formAction?: readonly string[];
|
|
29
|
+
/** Reporting group name for `report-to` (paired with a Reporting-Endpoints header). */
|
|
30
|
+
reportGroup?: string;
|
|
31
|
+
/** Legacy `report-uri` path (broader browser support during rollout). */
|
|
32
|
+
reportUri?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildCsp(opts: CspOptions): string {
|
|
36
|
+
const nonce = `'nonce-${opts.nonce}'`;
|
|
37
|
+
const directives: Record<string, string[]> = {
|
|
38
|
+
"default-src": ["'self'"],
|
|
39
|
+
"base-uri": ["'self'"],
|
|
40
|
+
"object-src": ["'none'"],
|
|
41
|
+
"frame-ancestors": ["'self'"],
|
|
42
|
+
"form-action": ["'self'", ...(opts.formAction ?? [])],
|
|
43
|
+
"script-src": ["'self'", nonce, ...(opts.scriptSrc ?? [])],
|
|
44
|
+
"style-src": ["'self'", nonce, ...(opts.styleSrc ?? [])],
|
|
45
|
+
// Inline style ATTRIBUTES (style="…") can't carry a nonce; CSS can't execute
|
|
46
|
+
// JS, so this is the one tolerated relaxation. style-src-elem still inherits
|
|
47
|
+
// the strict 'self'+nonce from style-src; script-src stays fully strict.
|
|
48
|
+
"style-src-attr": ["'unsafe-inline'"],
|
|
49
|
+
"font-src": ["'self'", ...(opts.fontSrc ?? [])],
|
|
50
|
+
"img-src": ["'self'", "data:", ...(opts.imgSrc ?? [])],
|
|
51
|
+
"connect-src": ["'self'", ...(opts.connectSrc ?? [])],
|
|
52
|
+
"frame-src": ["'self'", ...(opts.frameSrc ?? [])],
|
|
53
|
+
};
|
|
54
|
+
const parts = Object.entries(directives).map(
|
|
55
|
+
([name, values]) => `${name} ${values.join(" ")}`,
|
|
56
|
+
);
|
|
57
|
+
if (opts.reportUri) parts.push(`report-uri ${opts.reportUri}`);
|
|
58
|
+
if (opts.reportGroup) parts.push(`report-to ${opts.reportGroup}`);
|
|
59
|
+
return parts.join("; ");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Endpoint that collects CSP violation reports during the report-only rollout. */
|
|
63
|
+
export const CSP_REPORT_PATH = "/api/csp-report";
|
|
64
|
+
export const CSP_REPORT_GROUP = "csp-endpoint";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The storefront's known-good external origins. The middleware passes these to
|
|
68
|
+
* `buildCsp` and may extend `scriptSrc` (tenant hashes) + `frameSrc` (the widget
|
|
69
|
+
* origin) per request.
|
|
70
|
+
*/
|
|
71
|
+
export const STOREFRONT_CSP_ALLOWLIST = {
|
|
72
|
+
scriptSrc: ["https://static.cloudflareinsights.com"],
|
|
73
|
+
styleSrc: ["https://fonts.googleapis.com"],
|
|
74
|
+
fontSrc: ["https://fonts.gstatic.com"],
|
|
75
|
+
// Product imagery is rehosted to R2 or served from remote CDNs.
|
|
76
|
+
imgSrc: ["https:"],
|
|
77
|
+
connectSrc: ["https://cloudflareinsights.com"],
|
|
78
|
+
} as const;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* CSP additions for a FoxyCart-enabled tenant. The loader.js comes from
|
|
82
|
+
* `cdn.foxycart.com`; the sidecart XHR + iframe + the add-to-cart form POST all
|
|
83
|
+
* hit the store's own origin (`<store>.foxycart.com` or a branded checkout
|
|
84
|
+
* domain). Merge these into `buildCsp` per request when `locals.commerce.foxy`
|
|
85
|
+
* is present so the strict policy still passes.
|
|
86
|
+
*/
|
|
87
|
+
export function foxyCspAdditions(storeOrigin: string): {
|
|
88
|
+
scriptSrc: string[];
|
|
89
|
+
styleSrc: string[];
|
|
90
|
+
fontSrc: string[];
|
|
91
|
+
connectSrc: string[];
|
|
92
|
+
frameSrc: string[];
|
|
93
|
+
formAction: string[];
|
|
94
|
+
imgSrc: string[];
|
|
95
|
+
} {
|
|
96
|
+
const cdn = "https://cdn.foxycart.com";
|
|
97
|
+
// Foxy's loader pulls its own JS, CSS, and fonts from the CDN and renders the
|
|
98
|
+
// sidecart/checkout from the store origin — so it needs script/style/font from
|
|
99
|
+
// the CDN and connect/frame/form to the store.
|
|
100
|
+
return {
|
|
101
|
+
scriptSrc: [cdn, storeOrigin],
|
|
102
|
+
styleSrc: [cdn, storeOrigin],
|
|
103
|
+
fontSrc: [cdn, storeOrigin],
|
|
104
|
+
connectSrc: [cdn, storeOrigin],
|
|
105
|
+
frameSrc: [cdn, storeOrigin],
|
|
106
|
+
formAction: [storeOrigin],
|
|
107
|
+
imgSrc: [cdn, storeOrigin],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gated preview support: signing preview access and resolving which
|
|
3
|
+
* customization environment a request sees.
|
|
4
|
+
*
|
|
5
|
+
* Two pure, runtime-portable pieces (Web Crypto + btoa/atob, no `node:*`):
|
|
6
|
+
*
|
|
7
|
+
* 1. signed, short-TTL **preview tokens** (HMAC-SHA256) that grant access to a
|
|
8
|
+
* tenant's `test` customization on a preview host; and
|
|
9
|
+
* 2. `resolveCustomizationEnv` — the live-vs-test decision the middleware makes
|
|
10
|
+
* per request.
|
|
11
|
+
*
|
|
12
|
+
* Security properties this enforces:
|
|
13
|
+
* - **Production hosts never serve `test`.** A token is only consulted on a
|
|
14
|
+
* preview host, so a leaked token can't flip a live storefront.
|
|
15
|
+
* - **Tokens are tenant-bound.** A token minted for tenant A cannot preview
|
|
16
|
+
* tenant B (no cross-tenant leakage).
|
|
17
|
+
* - **Tokens expire.** `exp` is checked against the caller-supplied clock.
|
|
18
|
+
* - **Test is never indexable.** The resolution carries `noindex: true` so the
|
|
19
|
+
* middleware can set `X-Robots-Tag: noindex`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Claims carried by a preview token. */
|
|
23
|
+
export interface PreviewClaims {
|
|
24
|
+
tenantId: string;
|
|
25
|
+
env: "test";
|
|
26
|
+
/** Expiry, epoch seconds. */
|
|
27
|
+
exp: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const textEncoder = new TextEncoder();
|
|
31
|
+
|
|
32
|
+
function bytesToB64Url(bytes: Uint8Array): string {
|
|
33
|
+
let bin = "";
|
|
34
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
35
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function b64UrlToBytes(s: string): Uint8Array {
|
|
39
|
+
const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - (s.length % 4));
|
|
40
|
+
const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + pad);
|
|
41
|
+
const out = new Uint8Array(bin.length);
|
|
42
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function hmacKey(secret: string): Promise<CryptoKey> {
|
|
47
|
+
return crypto.subtle.importKey(
|
|
48
|
+
"raw",
|
|
49
|
+
textEncoder.encode(secret) as unknown as BufferSource,
|
|
50
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
51
|
+
false,
|
|
52
|
+
["sign", "verify"],
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Mint a preview token: `<payloadB64Url>.<sigB64Url>`. `ttlSeconds` is added to
|
|
58
|
+
* `nowSeconds` (caller supplies the clock so this stays pure/testable).
|
|
59
|
+
*/
|
|
60
|
+
export async function signPreviewToken(
|
|
61
|
+
secret: string,
|
|
62
|
+
tenantId: string,
|
|
63
|
+
ttlSeconds: number,
|
|
64
|
+
nowSeconds: number,
|
|
65
|
+
): Promise<string> {
|
|
66
|
+
const claims: PreviewClaims = {
|
|
67
|
+
tenantId,
|
|
68
|
+
env: "test",
|
|
69
|
+
exp: nowSeconds + ttlSeconds,
|
|
70
|
+
};
|
|
71
|
+
const payload = bytesToB64Url(textEncoder.encode(JSON.stringify(claims)));
|
|
72
|
+
const key = await hmacKey(secret);
|
|
73
|
+
const sig = await crypto.subtle.sign(
|
|
74
|
+
"HMAC",
|
|
75
|
+
key,
|
|
76
|
+
textEncoder.encode(payload) as unknown as BufferSource,
|
|
77
|
+
);
|
|
78
|
+
return `${payload}.${bytesToB64Url(new Uint8Array(sig))}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type VerifyResult =
|
|
82
|
+
| { ok: true; claims: PreviewClaims }
|
|
83
|
+
| { ok: false; reason: "malformed" | "bad-signature" | "expired" };
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Verify a preview token against `secret` and the caller's clock. Signature is
|
|
87
|
+
* checked with `crypto.subtle.verify` (constant-time) before expiry.
|
|
88
|
+
*/
|
|
89
|
+
export async function verifyPreviewToken(
|
|
90
|
+
secret: string,
|
|
91
|
+
token: string,
|
|
92
|
+
nowSeconds: number,
|
|
93
|
+
): Promise<VerifyResult> {
|
|
94
|
+
const dot = token.indexOf(".");
|
|
95
|
+
if (dot <= 0 || dot === token.length - 1) return { ok: false, reason: "malformed" };
|
|
96
|
+
const payload = token.slice(0, dot);
|
|
97
|
+
const sigPart = token.slice(dot + 1);
|
|
98
|
+
|
|
99
|
+
let sig: Uint8Array;
|
|
100
|
+
try {
|
|
101
|
+
sig = b64UrlToBytes(sigPart);
|
|
102
|
+
} catch {
|
|
103
|
+
return { ok: false, reason: "malformed" };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const key = await hmacKey(secret);
|
|
107
|
+
const valid = await crypto.subtle.verify(
|
|
108
|
+
"HMAC",
|
|
109
|
+
key,
|
|
110
|
+
sig as unknown as BufferSource,
|
|
111
|
+
textEncoder.encode(payload) as unknown as BufferSource,
|
|
112
|
+
);
|
|
113
|
+
if (!valid) return { ok: false, reason: "bad-signature" };
|
|
114
|
+
|
|
115
|
+
let claims: PreviewClaims;
|
|
116
|
+
try {
|
|
117
|
+
claims = JSON.parse(new TextDecoder().decode(b64UrlToBytes(payload)));
|
|
118
|
+
} catch {
|
|
119
|
+
return { ok: false, reason: "malformed" };
|
|
120
|
+
}
|
|
121
|
+
if (
|
|
122
|
+
!claims ||
|
|
123
|
+
claims.env !== "test" ||
|
|
124
|
+
typeof claims.tenantId !== "string" ||
|
|
125
|
+
typeof claims.exp !== "number"
|
|
126
|
+
) {
|
|
127
|
+
return { ok: false, reason: "malformed" };
|
|
128
|
+
}
|
|
129
|
+
if (claims.exp <= nowSeconds) return { ok: false, reason: "expired" };
|
|
130
|
+
return { ok: true, claims };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type CustomizationEnv = "live" | "test";
|
|
134
|
+
|
|
135
|
+
export interface EnvResolutionInput {
|
|
136
|
+
/** True when the request arrived on a preview host (e.g. `preview.*`). */
|
|
137
|
+
isPreviewHost: boolean;
|
|
138
|
+
/** The tenant resolved for this request. */
|
|
139
|
+
tenantId: string;
|
|
140
|
+
/** Token from `?__preview=` or the `tot_preview` cookie, if any. */
|
|
141
|
+
token?: string | null;
|
|
142
|
+
/** Platform preview-signing secret (env), if configured. */
|
|
143
|
+
secret?: string | null;
|
|
144
|
+
nowSeconds: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface EnvResolution {
|
|
148
|
+
env: CustomizationEnv;
|
|
149
|
+
/** Set `X-Robots-Tag: noindex` when true (test is never indexable). */
|
|
150
|
+
noindex: boolean;
|
|
151
|
+
/** When set, the middleware should short-circuit with this status. */
|
|
152
|
+
reject?: { status: number; message: string };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Decide which customization environment to serve. Production hosts always get
|
|
157
|
+
* `live` and never consult a token. Preview hosts are auth-gated: a valid,
|
|
158
|
+
* unexpired, tenant-matching token yields `test` (noindex); anything else is
|
|
159
|
+
* rejected.
|
|
160
|
+
*/
|
|
161
|
+
export async function resolveCustomizationEnv(
|
|
162
|
+
input: EnvResolutionInput,
|
|
163
|
+
): Promise<EnvResolution> {
|
|
164
|
+
if (!input.isPreviewHost) {
|
|
165
|
+
return { env: "live", noindex: false };
|
|
166
|
+
}
|
|
167
|
+
if (!input.secret) {
|
|
168
|
+
return {
|
|
169
|
+
env: "live",
|
|
170
|
+
noindex: true,
|
|
171
|
+
reject: { status: 503, message: "Preview is not configured." },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (!input.token) {
|
|
175
|
+
return {
|
|
176
|
+
env: "live",
|
|
177
|
+
noindex: true,
|
|
178
|
+
reject: { status: 401, message: "Preview token required." },
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const result = await verifyPreviewToken(input.secret, input.token, input.nowSeconds);
|
|
182
|
+
if (!result.ok) {
|
|
183
|
+
return {
|
|
184
|
+
env: "live",
|
|
185
|
+
noindex: true,
|
|
186
|
+
reject: { status: 401, message: `Invalid preview token (${result.reason}).` },
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (result.claims.tenantId !== input.tenantId) {
|
|
190
|
+
return {
|
|
191
|
+
env: "live",
|
|
192
|
+
noindex: true,
|
|
193
|
+
reject: { status: 403, message: "Preview token is for a different tenant." },
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return { env: "test", noindex: true };
|
|
197
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the tenant preview model: the `.tot/config.json` mapping shape and
|
|
3
|
+
* the preview-state record — which commit a tenant's preview targets vs. what is
|
|
4
|
+
* currently rendered — plus its KV key. Read-only shapes; no logic here.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Repo-relative path of the tenant repo's mapping config. */
|
|
8
|
+
export const TOT_CONFIG_PATH = ".tot/config.json";
|
|
9
|
+
|
|
10
|
+
/** One mapping from a tenant-repo prefix to a storefront artifact path. */
|
|
11
|
+
export interface TotRepoMapping {
|
|
12
|
+
/** Tenant-repo path/prefix (e.g. "theme.json", "content/", "public/"). */
|
|
13
|
+
workspace: string;
|
|
14
|
+
/** Storefront artifact path/prefix it maps to. */
|
|
15
|
+
repo: string;
|
|
16
|
+
kind: "file" | "tree";
|
|
17
|
+
}
|
|
18
|
+
export interface TotRepoConfig {
|
|
19
|
+
tenant: string;
|
|
20
|
+
scope?: string;
|
|
21
|
+
mappings: TotRepoMapping[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** One compliance check row (mirrors @tot/private-controlplane's EvidenceCheck —
|
|
25
|
+
* kept structural here so this read-only type stays dependency-free). */
|
|
26
|
+
export interface PreviewStateEvidenceCheck {
|
|
27
|
+
id: string;
|
|
28
|
+
label: string;
|
|
29
|
+
status: string;
|
|
30
|
+
detail?: string;
|
|
31
|
+
/** A concrete fix suggestion for a non-pass check (evidence producer's own —
|
|
32
|
+
* the same "why" + "fix" the CLI's `tot submit --watch` prints). */
|
|
33
|
+
hint?: string;
|
|
34
|
+
/** Whether this check is ship-blocking (REQUIRED_FOR_PROMOTE). Computed at
|
|
35
|
+
* merge time (preview-state-evidence.ts), not stored by the producer. */
|
|
36
|
+
required?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Fail-safe preview state — target/actual can diverge when target is broken. */
|
|
40
|
+
export interface PreviewState {
|
|
41
|
+
tenantId: string;
|
|
42
|
+
/** The commit the dev wants previewed. */
|
|
43
|
+
target: string | null;
|
|
44
|
+
/** The commit currently rendered (last successfully materialized). */
|
|
45
|
+
actual: string | null;
|
|
46
|
+
/** Why target != actual, when the target failed to render. Null on success. */
|
|
47
|
+
lastError: string | null;
|
|
48
|
+
lastReconcileAt: string | null;
|
|
49
|
+
/**
|
|
50
|
+
* The compliance verdict for the last successful reconcile (ADR 0010's evidence,
|
|
51
|
+
* mirrored here so the PUBLIC, unauthenticated /preview/dashboard page can show
|
|
52
|
+
* it — a shareable link should be as informative as the CLI's, without an MCP
|
|
53
|
+
* round trip). Optional/absent on a record `reconcilePreview` itself wrote
|
|
54
|
+
* (that pipeline doesn't know about evidence) — only the reconcile ROUTE's
|
|
55
|
+
* post-hoc merge sets it, after `reportPreviewEvidence` computes it. Absent and
|
|
56
|
+
* `null` both mean "no verdict yet"; callers should treat them the same.
|
|
57
|
+
*/
|
|
58
|
+
evidence?: { checks: PreviewStateEvidenceCheck[]; promotable: boolean; generatedAt: string } | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function previewStateKey(tenantId: string): string {
|
|
62
|
+
return `cust:preview-state:${tenantId}`;
|
|
63
|
+
}
|