@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,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture CatalogProducts for the style guide / component previews. Hard-coded so
|
|
3
|
+
* the commerce components render WITHOUT a live catalog. Each entry exercises a
|
|
4
|
+
* different card/price/badge state (default, sale, from-price, out-of-stock,
|
|
5
|
+
* compliant, new). Preview-only — never served on a real storefront surface.
|
|
6
|
+
*/
|
|
7
|
+
import type { CatalogProduct, CatalogVariant } from "@tot/public-runtime";
|
|
8
|
+
|
|
9
|
+
const NOW = "2026-07-01T00:00:00Z";
|
|
10
|
+
|
|
11
|
+
interface VariantSeed {
|
|
12
|
+
price: number;
|
|
13
|
+
compareAt?: number;
|
|
14
|
+
available?: boolean;
|
|
15
|
+
option?: Record<string, string>;
|
|
16
|
+
title?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function mkVariant(handle: string, i: number, v: VariantSeed): CatalogVariant {
|
|
20
|
+
return {
|
|
21
|
+
tenant_id: "sample",
|
|
22
|
+
id: `${handle}-v${i}`,
|
|
23
|
+
product_id: handle,
|
|
24
|
+
source: { platform: "shopify", source_id: `${handle}-v${i}` },
|
|
25
|
+
sku: `${handle.toUpperCase()}-${i}`,
|
|
26
|
+
title: v.title ?? "Default",
|
|
27
|
+
price: v.price,
|
|
28
|
+
compare_at_price: v.compareAt,
|
|
29
|
+
currency: "USD",
|
|
30
|
+
option_values: v.option ?? {},
|
|
31
|
+
inventory: { available: v.available ?? true },
|
|
32
|
+
position: i + 1,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ProductSeed {
|
|
37
|
+
handle: string;
|
|
38
|
+
title: string;
|
|
39
|
+
vendor?: string;
|
|
40
|
+
tags?: string[];
|
|
41
|
+
createdAt?: string;
|
|
42
|
+
options?: CatalogProduct["options"];
|
|
43
|
+
variants: VariantSeed[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function mkProduct(seed: ProductSeed): CatalogProduct {
|
|
47
|
+
const variants = seed.variants.map((v, i) => mkVariant(seed.handle, i, v));
|
|
48
|
+
const prices = variants.map((v) => v.price);
|
|
49
|
+
return {
|
|
50
|
+
tenant_id: "sample",
|
|
51
|
+
id: seed.handle,
|
|
52
|
+
source: { platform: "shopify", source_id: seed.handle },
|
|
53
|
+
handle: seed.handle,
|
|
54
|
+
title: seed.title,
|
|
55
|
+
description_html: `<p>${seed.title}</p>`,
|
|
56
|
+
description_text: seed.title,
|
|
57
|
+
vendor: seed.vendor,
|
|
58
|
+
product_type: "Sample",
|
|
59
|
+
tags: seed.tags ?? [],
|
|
60
|
+
status: "active",
|
|
61
|
+
seo: {},
|
|
62
|
+
options: seed.options ?? [],
|
|
63
|
+
images: [],
|
|
64
|
+
variants,
|
|
65
|
+
price_range: { min: Math.min(...prices), max: Math.max(...prices), currency: "USD" },
|
|
66
|
+
collections: ["sample"],
|
|
67
|
+
metafields: [],
|
|
68
|
+
created_at: seed.createdAt ?? NOW,
|
|
69
|
+
updated_at: NOW,
|
|
70
|
+
migrated_at: NOW,
|
|
71
|
+
source_checksum: seed.handle,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Six products, each showing a different card/price/badge permutation. */
|
|
76
|
+
export const sampleProducts: CatalogProduct[] = [
|
|
77
|
+
mkProduct({
|
|
78
|
+
handle: "sample-default",
|
|
79
|
+
title: "Classic Everyday Tee",
|
|
80
|
+
vendor: "Northwind",
|
|
81
|
+
tags: ["bestseller"],
|
|
82
|
+
variants: [{ price: 28 }],
|
|
83
|
+
}),
|
|
84
|
+
mkProduct({
|
|
85
|
+
handle: "sample-sale",
|
|
86
|
+
title: "Merino Zip Hoodie",
|
|
87
|
+
vendor: "Northwind",
|
|
88
|
+
tags: ["bestseller"],
|
|
89
|
+
variants: [{ price: 64, compareAt: 96 }],
|
|
90
|
+
}),
|
|
91
|
+
mkProduct({
|
|
92
|
+
handle: "sample-from",
|
|
93
|
+
title: "Trail Runner — All Sizes",
|
|
94
|
+
vendor: "Summit Co.",
|
|
95
|
+
options: [{ name: "Size", position: 1, values: ["S", "M", "L"] }],
|
|
96
|
+
variants: [
|
|
97
|
+
{ price: 42, option: { Size: "S" }, title: "S" },
|
|
98
|
+
{ price: 48, option: { Size: "M" }, title: "M" },
|
|
99
|
+
{ price: 55, option: { Size: "L" }, title: "L" },
|
|
100
|
+
],
|
|
101
|
+
}),
|
|
102
|
+
mkProduct({
|
|
103
|
+
handle: "sample-oos",
|
|
104
|
+
title: "Limited Canvas Pack",
|
|
105
|
+
vendor: "Field & Co.",
|
|
106
|
+
variants: [{ price: 120, available: false }],
|
|
107
|
+
}),
|
|
108
|
+
mkProduct({
|
|
109
|
+
handle: "sample-compliant",
|
|
110
|
+
title: "Age-Verified Reserve",
|
|
111
|
+
vendor: "Cellar 21",
|
|
112
|
+
tags: ["compliant", "bestseller"],
|
|
113
|
+
variants: [{ price: 89 }],
|
|
114
|
+
}),
|
|
115
|
+
mkProduct({
|
|
116
|
+
handle: "sample-new",
|
|
117
|
+
title: "Just-Dropped Utility Cap",
|
|
118
|
+
vendor: "Northwind",
|
|
119
|
+
tags: ["new"],
|
|
120
|
+
createdAt: NOW,
|
|
121
|
+
variants: [{ price: 24, compareAt: 32 }],
|
|
122
|
+
}),
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
/** Optional per-handle rating aggregates for the preview (source before use). */
|
|
126
|
+
export const sampleRatings: Record<string, { rating: number; count: number }> = {
|
|
127
|
+
"sample-default": { rating: 4.5, count: 128 },
|
|
128
|
+
"sample-sale": { rating: 4.8, count: 64 },
|
|
129
|
+
"sample-compliant": { rating: 5, count: 12 },
|
|
130
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Small presentation helpers shared across components. Tenant locale/currency aware. */
|
|
2
|
+
|
|
3
|
+
export function formatPrice(
|
|
4
|
+
amount: number,
|
|
5
|
+
currency: string,
|
|
6
|
+
locale = "en-US",
|
|
7
|
+
): string {
|
|
8
|
+
try {
|
|
9
|
+
return new Intl.NumberFormat(locale, {
|
|
10
|
+
style: "currency",
|
|
11
|
+
currency,
|
|
12
|
+
minimumFractionDigits: amount % 1 === 0 ? 0 : 2,
|
|
13
|
+
}).format(amount);
|
|
14
|
+
} catch {
|
|
15
|
+
return `${currency} ${amount.toFixed(2)}`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function discountPercent(price: number, compareAt?: number): number | null {
|
|
20
|
+
if (!compareAt || compareAt <= price) return null;
|
|
21
|
+
return Math.round(((compareAt - price) / compareAt) * 100);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Truncate plain text for meta descriptions / card copy. */
|
|
25
|
+
export function truncate(text: string, max = 160): string {
|
|
26
|
+
const clean = text.replace(/\s+/g, " ").trim();
|
|
27
|
+
if (clean.length <= max) return clean;
|
|
28
|
+
return clean.slice(0, max - 1).replace(/\s+\S*$/, "") + "…";
|
|
29
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-scoped FoxyCart commerce resolution (Track A).
|
|
3
|
+
*
|
|
4
|
+
* Bridges the tenant's static `commerce.foxy` config with runtime env so the
|
|
5
|
+
* demo store can be swapped without a code change and the signing secret stays
|
|
6
|
+
* out of source:
|
|
7
|
+
* - `FOXY_STORE_SUBDOMAIN` overrides the tenant's default subdomain (one env
|
|
8
|
+
* var points every commerce tenant at our shared demo store).
|
|
9
|
+
* - `FOXY_CART_SECRET` is the cart-validation secret used only in `signed`
|
|
10
|
+
* mode; the `unsigned-demo` mode never reads it (validation is OFF), which
|
|
11
|
+
* is what lets a developer run the whole cart locally with zero secrets.
|
|
12
|
+
*/
|
|
13
|
+
import type { TenantConfig, ResolvedFoxyCommerce } from "@tot/public-runtime";
|
|
14
|
+
import { resolveFoxyCommerce } from "@tot/public-runtime";
|
|
15
|
+
import { readEnv } from "@/lib/env";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the effective FoxyCart facts for a tenant, or null when the tenant has
|
|
19
|
+
* no store configured. Applies the `FOXY_STORE_SUBDOMAIN` override.
|
|
20
|
+
*/
|
|
21
|
+
export async function resolveTenantCommerce(
|
|
22
|
+
tenant: TenantConfig,
|
|
23
|
+
): Promise<ResolvedFoxyCommerce | null> {
|
|
24
|
+
const foxy = tenant.commerce?.foxy;
|
|
25
|
+
if (!foxy) return null;
|
|
26
|
+
// Per-environment overrides so the SAME tenant can point at different stores /
|
|
27
|
+
// checkout hosts by env: FOXY_STORE_SUBDOMAIN picks the Foxy store; FOXY_STORE_
|
|
28
|
+
// DOMAIN sets a branded custom checkout domain (e.g. checkout.preview.
|
|
29
|
+
// tokenoftrust.com on the shared test env, secure.<merchant>.com on live).
|
|
30
|
+
const subOverride = await readEnv("FOXY_STORE_SUBDOMAIN");
|
|
31
|
+
const domainOverride = await readEnv("FOXY_STORE_DOMAIN");
|
|
32
|
+
const storeSubDomain =
|
|
33
|
+
subOverride && subOverride.length ? subOverride : foxy.storeSubDomain;
|
|
34
|
+
const storeDomain =
|
|
35
|
+
domainOverride && domainOverride.length ? domainOverride : foxy.storeDomain;
|
|
36
|
+
return resolveFoxyCommerce({ ...foxy, storeSubDomain, storeDomain });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The cart-validation secret for signed mode. Returns undefined in
|
|
41
|
+
* `unsigned-demo` mode (never needed) or when unset. A signed-mode tenant with
|
|
42
|
+
* no secret is a misconfiguration the caller must handle (fall back to no cart).
|
|
43
|
+
*/
|
|
44
|
+
export async function readCartSecret(
|
|
45
|
+
commerce: ResolvedFoxyCommerce,
|
|
46
|
+
): Promise<string | undefined> {
|
|
47
|
+
if (commerce.mode !== "signed") return undefined;
|
|
48
|
+
return readEnv("FOXY_CART_SECRET");
|
|
49
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image render decision — testable render logic for <Img>.
|
|
3
|
+
*
|
|
4
|
+
* Picks the `src` + responsive `srcset` for a catalog image, factored out of
|
|
5
|
+
* Img.astro so the branching is unit-testable without rendering the component:
|
|
6
|
+
* - unusable/missing URL -> deterministic placeholder data URI
|
|
7
|
+
* - rehosted (R2) -> Cloudflare Image Resizing (/cdn-cgi/image, format=auto)
|
|
8
|
+
* - referenced Shopify CDN -> Shopify's native ?width= scheme
|
|
9
|
+
* - anything else usable -> the URL as-is, no srcset
|
|
10
|
+
*/
|
|
11
|
+
import { placeholderDataUri, isUsableImageUrl } from "./placeholder.js";
|
|
12
|
+
import { cfImageUrl, cfImageSrcset, CF_IMAGE_WIDTHS } from "./cfImage.js";
|
|
13
|
+
|
|
14
|
+
export interface ResolveImageInput {
|
|
15
|
+
src?: string;
|
|
16
|
+
rehosted?: boolean;
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
/** Placeholder seed (image id / product handle). */
|
|
20
|
+
seed: string;
|
|
21
|
+
label?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ResolvedImage {
|
|
25
|
+
src: string;
|
|
26
|
+
srcset?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const isShopifyCdnUrl = (url: string): boolean =>
|
|
30
|
+
/\/\/cdn\.shopify\.com\//.test(url) || /\/cdn\/shop\//.test(url);
|
|
31
|
+
|
|
32
|
+
export function resolveImage(input: ResolveImageInput): ResolvedImage {
|
|
33
|
+
const { src, rehosted = false, width, height, seed, label } = input;
|
|
34
|
+
|
|
35
|
+
if (!isUsableImageUrl(src, rehosted)) {
|
|
36
|
+
return { src: placeholderDataUri({ seed, width, height, label }) };
|
|
37
|
+
}
|
|
38
|
+
const url = src!;
|
|
39
|
+
|
|
40
|
+
// Rehosted (R2) images go through Cloudflare Image Resizing — true resizing +
|
|
41
|
+
// format=auto (AVIF/WebP).
|
|
42
|
+
if (rehosted) {
|
|
43
|
+
return { src: cfImageUrl(url, { width }), srcset: cfImageSrcset(url, width) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Referenced Shopify CDN URLs already resize via ?width=; keep that scheme.
|
|
47
|
+
if (isShopifyCdnUrl(url)) {
|
|
48
|
+
const srcset = CF_IMAGE_WIDTHS.filter((w) => w <= width * 2)
|
|
49
|
+
.map((w) => `${url}${url.includes("?") ? "&" : "?"}width=${w} ${w}w`)
|
|
50
|
+
.join(", ");
|
|
51
|
+
return { src: url, srcset };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { src: url };
|
|
55
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schema.org JSON-LD builders (brief §8): Product (with offers),
|
|
3
|
+
* BreadcrumbList, Organization. Kept pure so pages compose them and pass to <Seo>.
|
|
4
|
+
*/
|
|
5
|
+
import type { CatalogProduct, TenantConfig } from "@tot/public-runtime";
|
|
6
|
+
import { isAvailable } from "./tot/query.js";
|
|
7
|
+
|
|
8
|
+
export function organizationLd(tenant: TenantConfig, base: string) {
|
|
9
|
+
// Logo sourced from the migrated brand assets (og image / logo), so the
|
|
10
|
+
// Organization schema reflects the real merchant brand, not a generic favicon.
|
|
11
|
+
const brand = tenant.theme_tokens?.brand;
|
|
12
|
+
const logo =
|
|
13
|
+
brand?.ogImage ?? brand?.logoLight ?? new URL("/favicon.svg", base + "/").toString();
|
|
14
|
+
return {
|
|
15
|
+
"@context": "https://schema.org",
|
|
16
|
+
"@type": "Organization",
|
|
17
|
+
name: tenant.displayName,
|
|
18
|
+
url: base,
|
|
19
|
+
logo,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** ItemList — the ordered products on a collection/PLP page. */
|
|
24
|
+
export function itemListLd(
|
|
25
|
+
products: CatalogProduct[],
|
|
26
|
+
base: string,
|
|
27
|
+
pageOffset = 0,
|
|
28
|
+
) {
|
|
29
|
+
return {
|
|
30
|
+
"@context": "https://schema.org",
|
|
31
|
+
"@type": "ItemList",
|
|
32
|
+
numberOfItems: products.length,
|
|
33
|
+
itemListElement: products.map((p, i) => ({
|
|
34
|
+
"@type": "ListItem",
|
|
35
|
+
position: pageOffset + i + 1,
|
|
36
|
+
url: new URL(`/products/${p.handle}`, base + "/").toString(),
|
|
37
|
+
name: p.title,
|
|
38
|
+
})),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* WebSite + SearchAction — enables Google's sitelinks search box, wired to the
|
|
44
|
+
* storefront's own /search?q= endpoint.
|
|
45
|
+
*/
|
|
46
|
+
export function websiteLd(tenant: TenantConfig, base: string) {
|
|
47
|
+
return {
|
|
48
|
+
"@context": "https://schema.org",
|
|
49
|
+
"@type": "WebSite",
|
|
50
|
+
name: tenant.displayName,
|
|
51
|
+
url: base,
|
|
52
|
+
potentialAction: {
|
|
53
|
+
"@type": "SearchAction",
|
|
54
|
+
target: {
|
|
55
|
+
"@type": "EntryPoint",
|
|
56
|
+
urlTemplate: `${base.replace(/\/$/, "")}/search?q={search_term_string}`,
|
|
57
|
+
},
|
|
58
|
+
"query-input": "required name=search_term_string",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function breadcrumbLd(
|
|
64
|
+
crumbs: { label: string; href: string }[],
|
|
65
|
+
base: string,
|
|
66
|
+
) {
|
|
67
|
+
return {
|
|
68
|
+
"@context": "https://schema.org",
|
|
69
|
+
"@type": "BreadcrumbList",
|
|
70
|
+
itemListElement: crumbs.map((c, i) => ({
|
|
71
|
+
"@type": "ListItem",
|
|
72
|
+
position: i + 1,
|
|
73
|
+
name: c.label,
|
|
74
|
+
item: new URL(c.href, base + "/").toString(),
|
|
75
|
+
})),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function productLd(
|
|
80
|
+
p: CatalogProduct,
|
|
81
|
+
tenant: TenantConfig,
|
|
82
|
+
base: string,
|
|
83
|
+
) {
|
|
84
|
+
const url = new URL(`/products/${p.handle}`, base + "/").toString();
|
|
85
|
+
const images = p.images
|
|
86
|
+
.map((i) => i.url)
|
|
87
|
+
.filter((u) => !/cdn\.shopify\.com/.test(u)); // skip dead fixture URLs
|
|
88
|
+
|
|
89
|
+
const offers = p.variants.map((v) => ({
|
|
90
|
+
"@type": "Offer",
|
|
91
|
+
sku: v.sku,
|
|
92
|
+
price: v.price.toFixed(2),
|
|
93
|
+
priceCurrency: v.currency,
|
|
94
|
+
availability: v.inventory.available
|
|
95
|
+
? "https://schema.org/InStock"
|
|
96
|
+
: "https://schema.org/OutOfStock",
|
|
97
|
+
url,
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
const ratingMf = p.metafields.find(
|
|
101
|
+
(m) => m.namespace === "reviews" && m.key === "rating",
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
"@context": "https://schema.org",
|
|
106
|
+
"@type": "Product",
|
|
107
|
+
name: p.title,
|
|
108
|
+
description: p.seo.description || p.description_text.slice(0, 300),
|
|
109
|
+
sku: p.variants[0]?.sku,
|
|
110
|
+
...(images.length ? { image: images } : {}),
|
|
111
|
+
...(p.vendor ? { brand: { "@type": "Brand", name: p.vendor } } : {}),
|
|
112
|
+
...(p.product_type ? { category: p.product_type } : {}),
|
|
113
|
+
offers: {
|
|
114
|
+
"@type": "AggregateOffer",
|
|
115
|
+
priceCurrency: p.price_range.currency,
|
|
116
|
+
lowPrice: p.price_range.min.toFixed(2),
|
|
117
|
+
highPrice: p.price_range.max.toFixed(2),
|
|
118
|
+
offerCount: p.variants.length,
|
|
119
|
+
availability: isAvailable(p)
|
|
120
|
+
? "https://schema.org/InStock"
|
|
121
|
+
: "https://schema.org/OutOfStock",
|
|
122
|
+
offers,
|
|
123
|
+
},
|
|
124
|
+
...(ratingMf
|
|
125
|
+
? {
|
|
126
|
+
aggregateRating: {
|
|
127
|
+
"@type": "AggregateRating",
|
|
128
|
+
ratingValue: ratingMf.value,
|
|
129
|
+
bestRating: "5",
|
|
130
|
+
// ASSUMPTION: review count placeholder; real capture is Phase 2.
|
|
131
|
+
ratingCount: 24,
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
: {}),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site-wide maintenance kill-switch.
|
|
3
|
+
*
|
|
4
|
+
* When `MAINTENANCE_MODE` is truthy the middleware returns {@link maintenanceResponse}
|
|
5
|
+
* for EVERY host and path — before tenant resolution, the access gate, the edge
|
|
6
|
+
* cache, or any render. It is a deliberate full close (a "Coming Soon" holding
|
|
7
|
+
* page), NOT a login gate: auth paths are not exempt, no tenant is resolved, and
|
|
8
|
+
* nothing tenant-specific is served. Use it as the fail-safe lever to take the
|
|
9
|
+
* whole storefront offline in one config flip.
|
|
10
|
+
*
|
|
11
|
+
* Back online: set `MAINTENANCE_MODE=0` (or unset it) and redeploy.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** True for "1" / "on" / "true" / "yes" (case/space-insensitive); false otherwise. */
|
|
15
|
+
export function isMaintenanceOn(raw: string | null | undefined): boolean {
|
|
16
|
+
const v = (raw ?? "").trim().toLowerCase();
|
|
17
|
+
return v === "1" || v === "on" || v === "true" || v === "yes";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const MAINTENANCE_HTML = `<!doctype html>
|
|
21
|
+
<html lang="en">
|
|
22
|
+
<head>
|
|
23
|
+
<meta charset="utf-8" />
|
|
24
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
25
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
26
|
+
<title>Coming soon</title>
|
|
27
|
+
<style>
|
|
28
|
+
:root { color-scheme: light dark; }
|
|
29
|
+
* { box-sizing: border-box; }
|
|
30
|
+
html, body { height: 100%; margin: 0; }
|
|
31
|
+
body {
|
|
32
|
+
display: flex; align-items: center; justify-content: center;
|
|
33
|
+
min-height: 100%; padding: 2rem;
|
|
34
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
35
|
+
background: radial-gradient(1200px 800px at 50% -10%, #1e293b, #0f172a 60%, #020617);
|
|
36
|
+
color: #e2e8f0;
|
|
37
|
+
}
|
|
38
|
+
main { max-width: 34rem; text-align: center; }
|
|
39
|
+
.badge {
|
|
40
|
+
display: inline-block; letter-spacing: .18em; text-transform: uppercase;
|
|
41
|
+
font-size: .72rem; font-weight: 600; color: #94a3b8;
|
|
42
|
+
border: 1px solid rgba(148,163,184,.35); border-radius: 999px;
|
|
43
|
+
padding: .35rem .8rem; margin-bottom: 1.75rem;
|
|
44
|
+
}
|
|
45
|
+
h1 { font-size: clamp(2rem, 6vw, 3rem); line-height: 1.1; margin: 0 0 1rem; color: #f8fafc; }
|
|
46
|
+
p { font-size: 1.05rem; line-height: 1.6; color: #cbd5e1; margin: 0 auto; max-width: 28rem; }
|
|
47
|
+
.rule { width: 3rem; height: 3px; background: #38bdf8; border-radius: 2px; margin: 2rem auto 0; }
|
|
48
|
+
</style>
|
|
49
|
+
</head>
|
|
50
|
+
<body>
|
|
51
|
+
<main>
|
|
52
|
+
<span class="badge">Token of Trust</span>
|
|
53
|
+
<h1>Coming soon</h1>
|
|
54
|
+
<p>We're putting the finishing touches on the storefront. It'll be back online shortly — thanks for your patience.</p>
|
|
55
|
+
<div class="rule"></div>
|
|
56
|
+
</main>
|
|
57
|
+
</body>
|
|
58
|
+
</html>`;
|
|
59
|
+
|
|
60
|
+
/** A `503 Service Unavailable` holding page — cached nowhere, never indexed. */
|
|
61
|
+
export function maintenanceResponse(): Response {
|
|
62
|
+
return new Response(MAINTENANCE_HTML, {
|
|
63
|
+
status: 503,
|
|
64
|
+
headers: {
|
|
65
|
+
"content-type": "text/html; charset=utf-8",
|
|
66
|
+
"cache-control": "no-store",
|
|
67
|
+
"retry-after": "3600",
|
|
68
|
+
"x-robots-tag": "noindex, nofollow",
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory KVNamespace for local dev. Lets the real KvTenantResolver + putTenant
|
|
3
|
+
* run under `astro dev` (where no Cloudflare KV binding exists) so the dynamic
|
|
4
|
+
* tenant path and a data-driven default are testable without a Cloudflare
|
|
5
|
+
* account. NOT used in production — the Workers runtime binds the real
|
|
6
|
+
* TENANT_CACHE namespace. Process-local and non-persistent by design.
|
|
7
|
+
*/
|
|
8
|
+
export class MemoryKv implements KVNamespace {
|
|
9
|
+
private readonly store = new Map<string, string>();
|
|
10
|
+
|
|
11
|
+
async get(key: string): Promise<string | null> {
|
|
12
|
+
return this.store.has(key) ? (this.store.get(key) as string) : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async put(key: string, value: string): Promise<void> {
|
|
16
|
+
this.store.set(key, value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async list(
|
|
20
|
+
opts?: { prefix?: string; cursor?: string; limit?: number },
|
|
21
|
+
): Promise<{ keys: { name: string }[]; list_complete: boolean; cursor?: string }> {
|
|
22
|
+
const prefix = opts?.prefix ?? "";
|
|
23
|
+
const keys = [...this.store.keys()]
|
|
24
|
+
.filter((k) => k.startsWith(prefix))
|
|
25
|
+
.map((name) => ({ name }));
|
|
26
|
+
return { keys, list_complete: true };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async delete(key: string): Promise<void> {
|
|
30
|
+
this.store.delete(key);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessagesClient — the storefront's seam to tot20's inline-body send (`POST /api/messages`).
|
|
3
|
+
*
|
|
4
|
+
* The storefront RENDERS its own branded email HTML and hands it to tot20 to TRANSMIT, so we
|
|
5
|
+
* reuse tot20's Mandrill SPF/DKIM deliverability (the whole point) while owning the body/brand.
|
|
6
|
+
* Used for the magic-link invite email (see lib/grants/magicLinkInvite.ts).
|
|
7
|
+
*
|
|
8
|
+
* POST <core>/api/messages
|
|
9
|
+
* { channel:"email", to, subject, html, totApiKey, totSecretKey } → 200 { sent:true, id? }
|
|
10
|
+
*
|
|
11
|
+
* Creds go in the BODY (`totApiKey`/`totSecretKey`) — tot20's `normalizeSecureUserRequest`, same
|
|
12
|
+
* wire rule as the other operator clients. The storefront's operator key already holds
|
|
13
|
+
* `messaging:send`. DELIVERABILITY: do NOT set a non-`@tokenoftrust.com` From — omit it so the
|
|
14
|
+
* send stays SPF/DKIM-aligned with tot20's domain (a misaligned From lands in spam).
|
|
15
|
+
*
|
|
16
|
+
* Built like `@/lib/grants/developerInvites` (interface + Http impl + Mock + env factory).
|
|
17
|
+
*/
|
|
18
|
+
import { readEnv } from "@/lib/env";
|
|
19
|
+
|
|
20
|
+
export interface InlineEmail {
|
|
21
|
+
/** Recipient address. */
|
|
22
|
+
to: string;
|
|
23
|
+
/** Subject line (your copy). */
|
|
24
|
+
subject: string;
|
|
25
|
+
/** Fully-rendered branded HTML body (your template). */
|
|
26
|
+
html: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type MessageSendResult =
|
|
30
|
+
| { sent: true; id?: string }
|
|
31
|
+
| { sent: false; status: number; reason: string };
|
|
32
|
+
|
|
33
|
+
export interface MessagesClient {
|
|
34
|
+
sendInlineEmail(msg: InlineEmail): Promise<MessageSendResult>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// --- HTTP transport seam (injected by tests) --------------------------------
|
|
38
|
+
export interface HttpResponseLike {
|
|
39
|
+
status: number;
|
|
40
|
+
json(): Promise<unknown>;
|
|
41
|
+
}
|
|
42
|
+
export interface HttpTransport {
|
|
43
|
+
post(url: string, headers: Record<string, string>, body: unknown): Promise<HttpResponseLike>;
|
|
44
|
+
}
|
|
45
|
+
export class FetchHttpTransport implements HttpTransport {
|
|
46
|
+
async post(url: string, headers: Record<string, string>, body: unknown) {
|
|
47
|
+
return fetch(url, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: { "content-type": "application/json", accept: "application/json", ...headers },
|
|
50
|
+
body: JSON.stringify(body),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class MessagesClientError extends Error {}
|
|
56
|
+
|
|
57
|
+
export interface HttpMessagesClientConfig {
|
|
58
|
+
baseUrl: string;
|
|
59
|
+
apiKey: string;
|
|
60
|
+
secretKey?: string;
|
|
61
|
+
transport?: HttpTransport;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class HttpMessagesClient implements MessagesClient {
|
|
65
|
+
private readonly baseUrl: string;
|
|
66
|
+
private readonly apiKey: string;
|
|
67
|
+
private readonly secretKey?: string;
|
|
68
|
+
private readonly transport: HttpTransport;
|
|
69
|
+
|
|
70
|
+
constructor(config: HttpMessagesClientConfig) {
|
|
71
|
+
if (!config.baseUrl) throw new MessagesClientError("messages: TOT_CORE_URL is not configured");
|
|
72
|
+
if (!config.apiKey) throw new MessagesClientError("messages: storefront ToT credentials are not configured");
|
|
73
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
74
|
+
this.apiKey = config.apiKey;
|
|
75
|
+
this.secretKey = config.secretKey;
|
|
76
|
+
this.transport = config.transport ?? new FetchHttpTransport();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private credentialFields(): Record<string, string> {
|
|
80
|
+
const f: Record<string, string> = { totApiKey: this.apiKey };
|
|
81
|
+
if (this.secretKey) f.totSecretKey = this.secretKey;
|
|
82
|
+
return f;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async sendInlineEmail(msg: InlineEmail): Promise<MessageSendResult> {
|
|
86
|
+
let res: HttpResponseLike;
|
|
87
|
+
try {
|
|
88
|
+
res = await this.transport.post(
|
|
89
|
+
`${this.baseUrl}/api/messages`,
|
|
90
|
+
{},
|
|
91
|
+
{
|
|
92
|
+
channel: "email",
|
|
93
|
+
to: msg.to,
|
|
94
|
+
subject: msg.subject,
|
|
95
|
+
html: msg.html,
|
|
96
|
+
// No fromEmail/fromName → stays SPF/DKIM-aligned with tot20's domain (deliverability).
|
|
97
|
+
...this.credentialFields(),
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
} catch (cause) {
|
|
101
|
+
return { sent: false, status: 0, reason: cause instanceof Error ? cause.message : String(cause) };
|
|
102
|
+
}
|
|
103
|
+
let body: unknown;
|
|
104
|
+
try {
|
|
105
|
+
body = await res.json();
|
|
106
|
+
} catch {
|
|
107
|
+
return { sent: false, status: res.status, reason: `non-JSON body (status ${res.status})` };
|
|
108
|
+
}
|
|
109
|
+
return normalizeSend(res.status, body);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Parse tot20's `{ content, metadata }` send response into a typed result (fail closed). */
|
|
114
|
+
export function normalizeSend(status: number, body: unknown): MessageSendResult {
|
|
115
|
+
const raw = (body ?? {}) as Record<string, unknown>;
|
|
116
|
+
const c = (raw.content && typeof raw.content === "object" ? raw.content : raw) as Record<string, unknown>;
|
|
117
|
+
if (c.sent === true || (status >= 200 && status < 300 && c.sent !== false)) {
|
|
118
|
+
return { sent: true, ...(typeof c.id === "string" ? { id: c.id } : {}) };
|
|
119
|
+
}
|
|
120
|
+
const reason =
|
|
121
|
+
typeof c.message === "string" ? c.message : typeof c.reason === "string" ? c.reason : `send failed (status ${status})`;
|
|
122
|
+
return { sent: false, status, reason };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- Mock (tests + local dev) -----------------------------------------------
|
|
126
|
+
export class MockMessagesClient implements MessagesClient {
|
|
127
|
+
sent: InlineEmail[] = [];
|
|
128
|
+
constructor(private readonly result: MessageSendResult = { sent: true, id: "mock-msg" }) {}
|
|
129
|
+
async sendInlineEmail(msg: InlineEmail): Promise<MessageSendResult> {
|
|
130
|
+
this.sent.push(msg);
|
|
131
|
+
return this.result;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- Factory ----------------------------------------------------------------
|
|
136
|
+
export async function createMessagesClient(
|
|
137
|
+
overrides: Partial<HttpMessagesClientConfig> = {},
|
|
138
|
+
): Promise<HttpMessagesClient> {
|
|
139
|
+
const baseUrl = overrides.baseUrl ?? (await readEnv("TOT_CORE_URL")) ?? "";
|
|
140
|
+
const apiKey = overrides.apiKey ?? (await readEnv("TOT_API_KEY")) ?? "";
|
|
141
|
+
const secretKey = overrides.secretKey ?? (await readEnv("TOT_SECRET_KEY"));
|
|
142
|
+
return new HttpMessagesClient({ ...overrides, baseUrl, apiKey, secretKey });
|
|
143
|
+
}
|