@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,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slim per-product search document — the storefront's prebuilt search projection.
|
|
3
|
+
* The ETL emits an array of these per tenant;
|
|
4
|
+
* the storefront builds its Fuse index from them instead of pulling the full
|
|
5
|
+
* catalog per request. Keep this SMALL — it's loaded into the Worker and (later)
|
|
6
|
+
* stored in KV; full product detail is fetched per-handle only for what's shown.
|
|
7
|
+
*
|
|
8
|
+
* NOTE on store shape: a Fuse-able blob is the right shape for typo-tolerant
|
|
9
|
+
* SEARCH. Faceted filtering/listing is a different read model (D1/SQL). Don't
|
|
10
|
+
* grow this into a facet store.
|
|
11
|
+
*/
|
|
12
|
+
import type { CatalogProduct, ProductStatus } from "./product.js";
|
|
13
|
+
|
|
14
|
+
export interface SearchDoc {
|
|
15
|
+
handle: string;
|
|
16
|
+
title: string;
|
|
17
|
+
vendor?: string;
|
|
18
|
+
product_type?: string;
|
|
19
|
+
tags: string[];
|
|
20
|
+
description_text: string;
|
|
21
|
+
/** Lowest variant price (the "from" price). */
|
|
22
|
+
price_min: number;
|
|
23
|
+
currency: string;
|
|
24
|
+
/** First usable image URL, if any. */
|
|
25
|
+
image?: string;
|
|
26
|
+
status: ProductStatus;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Project a full CatalogProduct down to the slim search document. */
|
|
30
|
+
export function toSearchDoc(p: CatalogProduct): SearchDoc {
|
|
31
|
+
const doc: SearchDoc = {
|
|
32
|
+
handle: p.handle,
|
|
33
|
+
title: p.title,
|
|
34
|
+
tags: p.tags,
|
|
35
|
+
description_text: p.description_text,
|
|
36
|
+
price_min: p.price_range.min,
|
|
37
|
+
currency: p.price_range.currency,
|
|
38
|
+
status: p.status,
|
|
39
|
+
};
|
|
40
|
+
if (p.vendor) doc.vendor = p.vendor;
|
|
41
|
+
if (p.product_type) doc.product_type = p.product_type;
|
|
42
|
+
const img = p.images.find((i) => i.url) ?? p.images[0];
|
|
43
|
+
if (img?.url) doc.image = img.url;
|
|
44
|
+
return doc;
|
|
45
|
+
}
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Versioned tenant static-asset store (decision `assets-r2-versioned`).
|
|
3
|
+
*
|
|
4
|
+
* Tenant static assets (`tenants/<id>/public/**` — logo, CSS, ~36 binaries) are
|
|
5
|
+
* served from **R2, version-scoped**, replacing the build-time
|
|
6
|
+
* `copy-tenant-assets.mjs` baking for extracted/versioned tenants. A **version**
|
|
7
|
+
* is an immutable snapshot: every asset for a version lives under one R2 key
|
|
8
|
+
* namespace `tenants/<id>/<versionId>/<rel>`, and a per-tenant, per-channel
|
|
9
|
+
* pointer selects which snapshot `live` vs `test` serves. A pointer flip switches
|
|
10
|
+
* every asset in lockstep — the same channel→version model the content publish
|
|
11
|
+
* spine (`customization-versioning.ts`) uses, applied to the static tree that
|
|
12
|
+
* `reconcile` reports as `publicAssets` ("mapped but NOT KV-served, served
|
|
13
|
+
* separately").
|
|
14
|
+
*
|
|
15
|
+
* Pure + runtime-portable (Web Crypto, `atob`, no `node:*`) so it runs in the
|
|
16
|
+
* Astro/Cloudflare Worker, the publish/ingest pipeline, and unit tests alike. The
|
|
17
|
+
* concrete R2 binding is injected via the structural `R2BucketLike` seam (a real
|
|
18
|
+
* `R2Bucket` satisfies it; `MemoryR2Bucket` backs dev + tests).
|
|
19
|
+
*
|
|
20
|
+
* ── versionId contract (assumption to reconcile with teammate `publish`) ──────
|
|
21
|
+
* The snapshot `versionId` is supplied by the publish pipeline (so a version's
|
|
22
|
+
* assets and content share ONE id and flip together). When publish has not
|
|
23
|
+
* defined one, `computeAssetsVersionId` derives a deterministic id from the asset
|
|
24
|
+
* set (sha256 over the sorted `<rel>:sha256(bytes)` list) — self-contained and
|
|
25
|
+
* idempotent. The channel→version pointer is a dedicated KV record
|
|
26
|
+
* (`cust:assets-pointer:<id>` → `{ live, test, rev }`) so it composes with, but
|
|
27
|
+
* does not collide with, the content `PointerRecord`. See
|
|
28
|
+
* `docs/versioned-assets-contract.md`.
|
|
29
|
+
*/
|
|
30
|
+
import type { CustomizationEnv } from "./customization-preview.js";
|
|
31
|
+
|
|
32
|
+
/** sha256 as lowercase hex, via Web Crypto (Workers + Node 20+). Kept local so
|
|
33
|
+
* this renderer-safe module stays free of the control-plane versioning layer. */
|
|
34
|
+
async function sha256Hex(content: string | Uint8Array): Promise<string> {
|
|
35
|
+
const bytes =
|
|
36
|
+
typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
37
|
+
const digest = await crypto.subtle.digest(
|
|
38
|
+
"SHA-256",
|
|
39
|
+
bytes as unknown as BufferSource,
|
|
40
|
+
);
|
|
41
|
+
return Array.from(new Uint8Array(digest))
|
|
42
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
43
|
+
.join("");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// R2 seam — structural subset a real R2Bucket satisfies (no workers-types dep)
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
export interface R2ObjectLike {
|
|
51
|
+
/** Streaming body (real R2). Null for a HEAD-style object. */
|
|
52
|
+
body: ReadableStream<Uint8Array> | null;
|
|
53
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
54
|
+
/** Byte length, when the store knows it. */
|
|
55
|
+
size?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface R2BucketLike {
|
|
59
|
+
get(key: string): Promise<R2ObjectLike | null>;
|
|
60
|
+
put(
|
|
61
|
+
key: string,
|
|
62
|
+
value: ArrayBuffer | ArrayBufferView | ReadableStream | string,
|
|
63
|
+
): Promise<unknown>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Channel→version pointer (one snapshot versionId per channel)
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
/** Per-tenant asset pointer: the live/test snapshot versionId + a CAS token. */
|
|
71
|
+
export interface AssetsPointer {
|
|
72
|
+
tenantId: string;
|
|
73
|
+
/** Optimistic-concurrency token; bumped on every successful write. */
|
|
74
|
+
rev: number;
|
|
75
|
+
/** Snapshot versionId selected for the live channel (null = none published). */
|
|
76
|
+
live: string | null;
|
|
77
|
+
/** Snapshot versionId selected for the test channel. */
|
|
78
|
+
test: string | null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function emptyAssetsPointer(tenantId: string): AssetsPointer {
|
|
82
|
+
return { tenantId, rev: 0, live: null, test: null };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** KV key for a tenant's asset pointer (mirrors the `cust:*` layout). */
|
|
86
|
+
export function assetsPointerKey(tenantId: string): string {
|
|
87
|
+
return `cust:assets-pointer:${tenantId}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Resolve the snapshot versionId a channel serves (null = nothing published). */
|
|
91
|
+
export function resolveAssetVersion(
|
|
92
|
+
pointer: AssetsPointer,
|
|
93
|
+
channel: CustomizationEnv,
|
|
94
|
+
): string | null {
|
|
95
|
+
return channel === "test" ? pointer.test : pointer.live;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// R2 key scheme + path normalization
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
/** Reject empty, absolute, or traversal-bearing relative paths. */
|
|
103
|
+
function isSafeRel(rel: string): boolean {
|
|
104
|
+
if (!rel || rel.startsWith("/")) return false;
|
|
105
|
+
return !rel.split("/").some((seg) => seg === ".." || seg === ".");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* R2 object key for one asset of a version: `tenants/<id>/<versionId>/<rel>`.
|
|
110
|
+
* Throws on an unsafe `rel` (empty / absolute / `..`) so a crafted request can
|
|
111
|
+
* never escape the version namespace.
|
|
112
|
+
*/
|
|
113
|
+
export function tenantAssetKey(
|
|
114
|
+
tenantId: string,
|
|
115
|
+
versionId: string,
|
|
116
|
+
rel: string,
|
|
117
|
+
): string {
|
|
118
|
+
if (!isSafeRel(rel)) {
|
|
119
|
+
throw new Error(`unsafe tenant-asset path: ${JSON.stringify(rel)}`);
|
|
120
|
+
}
|
|
121
|
+
return `tenants/${tenantId}/${versionId}/${rel}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function escapeRe(s: string): string {
|
|
125
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Normalize a source path to the tenant's public-relative path (the `<rel>` in
|
|
130
|
+
* the R2 key), or null when the path is not a public asset for this tenant.
|
|
131
|
+
* Accepts both the colocated workspace shape (`tenants/<id>/public/<rel>`, from
|
|
132
|
+
* seed's `buildSeedFiles`) and the mapped artifact shape
|
|
133
|
+
* (`apps/storefront/public/tenants/<id>/<rel>`, reconcile's `publicAssets`).
|
|
134
|
+
*/
|
|
135
|
+
export function toPublicRel(tenantId: string, path: string): string | null {
|
|
136
|
+
const id = escapeRe(tenantId);
|
|
137
|
+
// apps/storefront/public/tenants/<id>/<rel> (mapped artifact)
|
|
138
|
+
const m1 = path.match(new RegExp(`(?:^|/)public/tenants/${id}/(.+)$`))?.[1];
|
|
139
|
+
if (m1 && isSafeRel(m1)) return m1;
|
|
140
|
+
// tenants/<id>/public/<rel> (colocated workspace)
|
|
141
|
+
const m2 = path.match(new RegExp(`(?:^|/)tenants/${id}/public/(.+)$`))?.[1];
|
|
142
|
+
if (m2 && isSafeRel(m2)) return m2;
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Parse a request pathname `/tenants/<id>/<rel...>` into its tenant + relative
|
|
148
|
+
* asset path, or null when the shape does not match (so the caller can fall
|
|
149
|
+
* through). `<rel>` is validated: a traversal attempt yields null.
|
|
150
|
+
*/
|
|
151
|
+
export function parseTenantAssetPath(
|
|
152
|
+
pathname: string,
|
|
153
|
+
): { tenantId: string; rel: string } | null {
|
|
154
|
+
let p = pathname;
|
|
155
|
+
try {
|
|
156
|
+
p = decodeURIComponent(pathname);
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const m = p.match(/^\/tenants\/([^/]+)\/(.+)$/);
|
|
161
|
+
const tenantId = m?.[1];
|
|
162
|
+
const rel = m?.[2];
|
|
163
|
+
if (!tenantId || !rel || !isSafeRel(rel)) return null;
|
|
164
|
+
return { tenantId, rel };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Content-type + cache headers
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
const CONTENT_TYPES: Record<string, string> = {
|
|
172
|
+
css: "text/css; charset=utf-8",
|
|
173
|
+
js: "text/javascript; charset=utf-8",
|
|
174
|
+
mjs: "text/javascript; charset=utf-8",
|
|
175
|
+
json: "application/json; charset=utf-8",
|
|
176
|
+
html: "text/html; charset=utf-8",
|
|
177
|
+
txt: "text/plain; charset=utf-8",
|
|
178
|
+
xml: "application/xml; charset=utf-8",
|
|
179
|
+
svg: "image/svg+xml",
|
|
180
|
+
png: "image/png",
|
|
181
|
+
jpg: "image/jpeg",
|
|
182
|
+
jpeg: "image/jpeg",
|
|
183
|
+
webp: "image/webp",
|
|
184
|
+
avif: "image/avif",
|
|
185
|
+
gif: "image/gif",
|
|
186
|
+
ico: "image/x-icon",
|
|
187
|
+
woff: "font/woff",
|
|
188
|
+
woff2: "font/woff2",
|
|
189
|
+
ttf: "font/ttf",
|
|
190
|
+
otf: "font/otf",
|
|
191
|
+
eot: "application/vnd.ms-fontobject",
|
|
192
|
+
pdf: "application/pdf",
|
|
193
|
+
mp4: "video/mp4",
|
|
194
|
+
webm: "video/webm",
|
|
195
|
+
mov: "video/quicktime",
|
|
196
|
+
wasm: "application/wasm",
|
|
197
|
+
map: "application/json; charset=utf-8",
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/** Best-effort content-type from a relative path's extension. */
|
|
201
|
+
export function assetContentType(rel: string): string {
|
|
202
|
+
const dot = rel.lastIndexOf(".");
|
|
203
|
+
const ext = dot >= 0 ? rel.slice(dot + 1).toLowerCase() : "";
|
|
204
|
+
return CONTENT_TYPES[ext] ?? "application/octet-stream";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Cache-control for a versioned asset. The URL path is stable but its BYTES flip
|
|
209
|
+
* with the pointer, so the resource at `/tenants/<id>/<rel>` is NOT immutable —
|
|
210
|
+
* we cache for a short window and revalidate. (The R2 KEY is immutable because
|
|
211
|
+
* versionId is content/snapshot-scoped, but the public URL is not versioned.)
|
|
212
|
+
*/
|
|
213
|
+
export const ASSET_CACHE_CONTROL =
|
|
214
|
+
"public, max-age=300, s-maxage=300, stale-while-revalidate=86400";
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// Ingest — write a version's public assets to R2
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
/** One input file (seed's `buildSeedFiles` shape, or a reconcile publicAsset). */
|
|
221
|
+
export interface AssetFile {
|
|
222
|
+
/** Source path (workspace or mapped artifact form — see `toPublicRel`). */
|
|
223
|
+
path: string;
|
|
224
|
+
/** How `content` is encoded. Bytes bypass this. Default "utf8". */
|
|
225
|
+
encoding?: "utf8" | "base64";
|
|
226
|
+
content: string | Uint8Array;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function decodeContent(f: AssetFile): Uint8Array {
|
|
230
|
+
if (f.content instanceof Uint8Array) return f.content;
|
|
231
|
+
if (f.encoding === "base64") {
|
|
232
|
+
const bin = atob(f.content);
|
|
233
|
+
const out = new Uint8Array(bin.length);
|
|
234
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
return new TextEncoder().encode(f.content);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** The public-asset subset of a file list, normalized to `{ rel, bytes }`. */
|
|
241
|
+
export function selectPublicAssets(
|
|
242
|
+
tenantId: string,
|
|
243
|
+
files: AssetFile[],
|
|
244
|
+
): Array<{ rel: string; bytes: Uint8Array }> {
|
|
245
|
+
const out: Array<{ rel: string; bytes: Uint8Array }> = [];
|
|
246
|
+
for (const f of files) {
|
|
247
|
+
const rel = toPublicRel(tenantId, f.path);
|
|
248
|
+
if (rel == null) continue;
|
|
249
|
+
out.push({ rel, bytes: decodeContent(f) });
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Deterministic snapshot versionId derived purely from the asset set: sha256
|
|
256
|
+
* over the sorted `<rel>:sha256(bytes)` lines. Idempotent + self-contained — the
|
|
257
|
+
* default when the publish pipeline has not supplied its own snapshot id.
|
|
258
|
+
*/
|
|
259
|
+
export async function computeAssetsVersionId(
|
|
260
|
+
tenantId: string,
|
|
261
|
+
files: AssetFile[],
|
|
262
|
+
): Promise<string> {
|
|
263
|
+
const assets = selectPublicAssets(tenantId, files);
|
|
264
|
+
const lines = await Promise.all(
|
|
265
|
+
assets.map(async (a) => `${a.rel}:${await sha256Hex(a.bytes)}`),
|
|
266
|
+
);
|
|
267
|
+
lines.sort();
|
|
268
|
+
return sha256Hex(lines.join("\n"));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface IngestResult {
|
|
272
|
+
versionId: string;
|
|
273
|
+
/** R2 keys written, in input order. */
|
|
274
|
+
keys: string[];
|
|
275
|
+
count: number;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Write a version's public assets to R2 under `tenants/<id>/<versionId>/<rel>`.
|
|
280
|
+
* Content-addressed by the (caller-supplied or computed) snapshot id → immutable
|
|
281
|
+
* + idempotent: re-ingesting the same version overwrites identical bytes (no-op).
|
|
282
|
+
* Non-public files in `files` are ignored, so the whole seed/reconcile file set
|
|
283
|
+
* can be passed. Pass `versionId` to bind assets to the publish snapshot id;
|
|
284
|
+
* omit it to derive one from the bytes.
|
|
285
|
+
*/
|
|
286
|
+
export async function ingestAssetVersion(
|
|
287
|
+
bucket: R2BucketLike,
|
|
288
|
+
tenantId: string,
|
|
289
|
+
files: AssetFile[],
|
|
290
|
+
versionId?: string,
|
|
291
|
+
): Promise<IngestResult> {
|
|
292
|
+
const vid = versionId ?? (await computeAssetsVersionId(tenantId, files));
|
|
293
|
+
const assets = selectPublicAssets(tenantId, files);
|
|
294
|
+
const keys: string[] = [];
|
|
295
|
+
for (const a of assets) {
|
|
296
|
+
const key = tenantAssetKey(tenantId, vid, a.rel);
|
|
297
|
+
await bucket.put(key, a.bytes);
|
|
298
|
+
keys.push(key);
|
|
299
|
+
}
|
|
300
|
+
return { versionId: vid, keys, count: keys.length };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
// Pointer persistence (KV) — read/write the channel→version selection
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
export interface AssetsKv {
|
|
308
|
+
get(key: string): Promise<string | null>;
|
|
309
|
+
put(key: string, value: string): Promise<void>;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export async function readAssetsPointer(
|
|
313
|
+
kv: Pick<AssetsKv, "get">,
|
|
314
|
+
tenantId: string,
|
|
315
|
+
): Promise<AssetsPointer> {
|
|
316
|
+
const raw = await kv.get(assetsPointerKey(tenantId));
|
|
317
|
+
if (!raw) return emptyAssetsPointer(tenantId);
|
|
318
|
+
try {
|
|
319
|
+
const p = JSON.parse(raw) as Partial<AssetsPointer>;
|
|
320
|
+
return {
|
|
321
|
+
tenantId,
|
|
322
|
+
rev: typeof p.rev === "number" ? p.rev : 0,
|
|
323
|
+
live: typeof p.live === "string" ? p.live : null,
|
|
324
|
+
test: typeof p.test === "string" ? p.test : null,
|
|
325
|
+
};
|
|
326
|
+
} catch {
|
|
327
|
+
// Fail safe: a malformed record reads as empty (never wedges writes).
|
|
328
|
+
return emptyAssetsPointer(tenantId);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Point a channel at a snapshot versionId (read-check-write on `rev`). KV has no
|
|
334
|
+
* hardware CAS; a single serialized writer per tenant (the publish/reconcile
|
|
335
|
+
* pipeline) makes the optimistic rev check sufficient — same trade-off as
|
|
336
|
+
* `KVVersionStore.setPointer`.
|
|
337
|
+
*/
|
|
338
|
+
export async function setAssetsChannel(
|
|
339
|
+
kv: AssetsKv,
|
|
340
|
+
tenantId: string,
|
|
341
|
+
channel: CustomizationEnv,
|
|
342
|
+
versionId: string,
|
|
343
|
+
): Promise<AssetsPointer> {
|
|
344
|
+
const current = await readAssetsPointer(kv, tenantId);
|
|
345
|
+
const next: AssetsPointer = {
|
|
346
|
+
...current,
|
|
347
|
+
rev: current.rev + 1,
|
|
348
|
+
[channel]: versionId,
|
|
349
|
+
};
|
|
350
|
+
await kv.put(assetsPointerKey(tenantId), JSON.stringify(next));
|
|
351
|
+
return next;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Promote the test snapshot to live (atomic pointer flip). */
|
|
355
|
+
export async function promoteAssets(
|
|
356
|
+
kv: AssetsKv,
|
|
357
|
+
tenantId: string,
|
|
358
|
+
): Promise<AssetsPointer> {
|
|
359
|
+
const current = await readAssetsPointer(kv, tenantId);
|
|
360
|
+
const next: AssetsPointer = {
|
|
361
|
+
...current,
|
|
362
|
+
rev: current.rev + 1,
|
|
363
|
+
live: current.test,
|
|
364
|
+
};
|
|
365
|
+
await kv.put(assetsPointerKey(tenantId), JSON.stringify(next));
|
|
366
|
+
return next;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
// Serve — resolve version, stream from R2 with headers
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
export interface ServeTenantAssetDeps {
|
|
374
|
+
bucket: R2BucketLike;
|
|
375
|
+
/** KV read for the assets pointer. */
|
|
376
|
+
kvGet: (key: string) => Promise<string | null>;
|
|
377
|
+
/** Channel to serve; default "live". Pass `locals.customizationEnv`. */
|
|
378
|
+
channel?: CustomizationEnv;
|
|
379
|
+
/** Request `If-None-Match` header, for conditional GET (304). */
|
|
380
|
+
ifNoneMatch?: string | null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Serve `/tenants/<id>/<rel>` from the version-scoped R2 store.
|
|
385
|
+
*
|
|
386
|
+
* Returns `null` — meaning "not handled here, fall through" — when the path is
|
|
387
|
+
* not a tenant-asset path OR the tenant has no published asset version for the
|
|
388
|
+
* channel (the back-compat gate: non-versioned tenants are served by the baked
|
|
389
|
+
* static tree, which the adapter matches BEFORE this SSR route). Returns a 404
|
|
390
|
+
* `Response` when a version IS selected but the object is missing. On a hit,
|
|
391
|
+
* streams the object with the resolved content-type, a revalidating cache
|
|
392
|
+
* header, and a versionId ETag (honoring `If-None-Match` with a 304).
|
|
393
|
+
*/
|
|
394
|
+
export async function serveTenantAsset(
|
|
395
|
+
deps: ServeTenantAssetDeps,
|
|
396
|
+
pathname: string,
|
|
397
|
+
): Promise<Response | null> {
|
|
398
|
+
const parsed = parseTenantAssetPath(pathname);
|
|
399
|
+
if (!parsed) return null;
|
|
400
|
+
const { tenantId, rel } = parsed;
|
|
401
|
+
|
|
402
|
+
const pointer = await readAssetsPointer({ get: deps.kvGet }, tenantId);
|
|
403
|
+
const versionId = resolveAssetVersion(pointer, deps.channel ?? "live");
|
|
404
|
+
if (!versionId) return null; // non-versioned tenant → fall through
|
|
405
|
+
|
|
406
|
+
const etag = `"${versionId}"`;
|
|
407
|
+
const baseHeaders: Record<string, string> = {
|
|
408
|
+
"cache-control": ASSET_CACHE_CONTROL,
|
|
409
|
+
etag,
|
|
410
|
+
vary: "If-None-Match",
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
if (deps.ifNoneMatch && matchesEtag(deps.ifNoneMatch, etag)) {
|
|
414
|
+
return new Response(null, { status: 304, headers: baseHeaders });
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const key = tenantAssetKey(tenantId, versionId, rel);
|
|
418
|
+
const object = await deps.bucket.get(key);
|
|
419
|
+
if (!object) {
|
|
420
|
+
return new Response("Not found", {
|
|
421
|
+
status: 404,
|
|
422
|
+
headers: { "cache-control": "no-store" },
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const headers: Record<string, string> = {
|
|
427
|
+
...baseHeaders,
|
|
428
|
+
"content-type": assetContentType(rel),
|
|
429
|
+
};
|
|
430
|
+
if (typeof object.size === "number") {
|
|
431
|
+
headers["content-length"] = String(object.size);
|
|
432
|
+
}
|
|
433
|
+
const body: BodyInit = object.body ?? (await object.arrayBuffer());
|
|
434
|
+
return new Response(body, { status: 200, headers });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Weak `If-None-Match` compare: `*`, exact, or member of a comma list. */
|
|
438
|
+
function matchesEtag(ifNoneMatch: string, etag: string): boolean {
|
|
439
|
+
const norm = (s: string) => s.trim().replace(/^W\//, "");
|
|
440
|
+
const target = norm(etag);
|
|
441
|
+
return ifNoneMatch
|
|
442
|
+
.split(",")
|
|
443
|
+
.some((t) => t.trim() === "*" || norm(t) === target);
|
|
444
|
+
}
|