@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,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TotAccessClient — the storefront's seam to tot20's two access-code endpoints
|
|
3
|
+
* that back "Sign in with Token of Trust" for a gated preview host:
|
|
4
|
+
*
|
|
5
|
+
* POST /api/internal/access-code/send { email, resource }
|
|
6
|
+
* → always 200 { ok: true } (neutral; tot20 mails a code only if the
|
|
7
|
+
* email holds a role on `resource` — enumeration-resistant)
|
|
8
|
+
* POST /api/internal/access-code/verify { email, resource, code }
|
|
9
|
+
* → { ok:true, email, resource, roles[], capability } on success
|
|
10
|
+
* → { ok:false, reason } otherwise
|
|
11
|
+
*
|
|
12
|
+
* tot20 is the single authority: it owns the email→role map per appDomain (the
|
|
13
|
+
* grant check) AND the one-time code store. The storefront just relays and holds
|
|
14
|
+
* the resulting session. See /tmp/tot20-access-code-handoff.md for the contract.
|
|
15
|
+
*
|
|
16
|
+
* Built like `@/lib/forge` (interface + Http impl + Mock + env factory) so routes
|
|
17
|
+
* develop/test against `MockTotAccessClient` before the tot20 endpoints exist,
|
|
18
|
+
* then swap in the Http client with no call-site change.
|
|
19
|
+
*
|
|
20
|
+
* INTEGRATION NOTE (RESOLVED against live qa 2026-07-05): tot20's swagger
|
|
21
|
+
* `normalizeSecureUserRequest()` extracts machine creds from the request
|
|
22
|
+
* BODY/QUERY fields `totApiKey`/`totSecretKey` (or an `Authorization` bearer/
|
|
23
|
+
* license-key header) — NOT from custom `x-tot-*` headers. Verified empirically:
|
|
24
|
+
* creds in the body advance past auth (`problemFindingApiClient` for an unknown
|
|
25
|
+
* key) whereas header creds are rejected (`apiKeyRequired`). The wire is isolated
|
|
26
|
+
* to `credentialFields()` below — the ONLY spot to touch. Everything else (paths,
|
|
27
|
+
* bodies, response shapes) is contract-fixed.
|
|
28
|
+
*/
|
|
29
|
+
import { readEnv } from "@/lib/env";
|
|
30
|
+
import { diagEmail } from "@/lib/diag/redact";
|
|
31
|
+
|
|
32
|
+
/** `send` is deliberately opaque — it never reveals whether the email was eligible. */
|
|
33
|
+
export interface AccessCodeSendResult {
|
|
34
|
+
ok: true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** `verify` echoes the roles/capability tot20 resolved for the email on `resource`. */
|
|
38
|
+
export type AccessCodeVerifyResult =
|
|
39
|
+
| {
|
|
40
|
+
ok: true;
|
|
41
|
+
email: string;
|
|
42
|
+
resource: string;
|
|
43
|
+
roles: string[];
|
|
44
|
+
capability: string;
|
|
45
|
+
/**
|
|
46
|
+
* The signed ES256 identity assertion (compact JWS) tot emits at sign-in.
|
|
47
|
+
* Present-usually, NOT guaranteed: tot returns the flat result without it
|
|
48
|
+
* if signing hiccups (contract §6 graceful degradation). Phase 3 verifies
|
|
49
|
+
* this OFFLINE and derives the session from its claims rather than trusting
|
|
50
|
+
* the flat `roles`/`capability`. See `@/lib/auth/identityToken`.
|
|
51
|
+
*/
|
|
52
|
+
id_token?: string;
|
|
53
|
+
}
|
|
54
|
+
| { ok: false; reason: VerifyFailureReason };
|
|
55
|
+
|
|
56
|
+
export type VerifyFailureReason =
|
|
57
|
+
| "invalid"
|
|
58
|
+
| "expired"
|
|
59
|
+
| "too_many_attempts"
|
|
60
|
+
| "error";
|
|
61
|
+
|
|
62
|
+
export interface TotAccessClient {
|
|
63
|
+
/** Ask tot20 to email a one-time code (only sent if the email is eligible). */
|
|
64
|
+
sendAccessCode(email: string, resource: string): Promise<AccessCodeSendResult>;
|
|
65
|
+
/** Verify the code; on success returns the viewer's roles + effective capability. */
|
|
66
|
+
verifyAccessCode(
|
|
67
|
+
email: string,
|
|
68
|
+
resource: string,
|
|
69
|
+
code: string,
|
|
70
|
+
): Promise<AccessCodeVerifyResult>;
|
|
71
|
+
/**
|
|
72
|
+
* FEDERATED sign-in (multi-tenant): email a one-time code WITHOUT a resource —
|
|
73
|
+
* the caller (storefront, via its operator cred) is the audience; tot20 mails
|
|
74
|
+
* the code if the email holds a role on ANY tenant. Enumeration-resistant.
|
|
75
|
+
*/
|
|
76
|
+
sendFederatedCode(email: string): Promise<AccessCodeSendResult>;
|
|
77
|
+
/**
|
|
78
|
+
* FEDERATED verify: on success the `id_token` carries ALL of the email's tenant
|
|
79
|
+
* memberships and `aud` = the calling app (storefront), not a tenant. `audience`
|
|
80
|
+
* echoes that aud so the caller can cross-check its configured app identity.
|
|
81
|
+
*/
|
|
82
|
+
verifyFederatedCode(
|
|
83
|
+
email: string,
|
|
84
|
+
code: string,
|
|
85
|
+
): Promise<FederatedVerifyResult>;
|
|
86
|
+
/**
|
|
87
|
+
* MAGIC-LINK exchange: exchange a single-use `signInToken` (minted by
|
|
88
|
+
* `developerInvites` with `issueSignInToken:true`) for a sign-in. POST-only by
|
|
89
|
+
* contract — never call this from a GET path (inbox link-scanners fire GET and would
|
|
90
|
+
* burn the token). Same result shape as {@link verifyFederatedCode}: the signed
|
|
91
|
+
* `id_token` is the SOLE authoritative output. `audience` is bound to the
|
|
92
|
+
* authenticated caller server-side, never passed in the body.
|
|
93
|
+
*/
|
|
94
|
+
exchangeSignInToken(token: string): Promise<FederatedVerifyResult>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Result of the FEDERATED verify. Success carries the signed `id_token` (whose
|
|
99
|
+
* claims — email + all `tenants[]` — are the sole authority) and the `audience`
|
|
100
|
+
* tot set (= the calling app). `id_token` is present-usually, not guaranteed
|
|
101
|
+
* (signer degradation, contract §6); the consumer fails closed when it's absent.
|
|
102
|
+
*/
|
|
103
|
+
export type FederatedVerifyResult =
|
|
104
|
+
| { ok: true; email: string; audience: string; id_token?: string }
|
|
105
|
+
| { ok: false; reason: VerifyFailureReason };
|
|
106
|
+
|
|
107
|
+
// --- HTTP transport seam (injected by tests) --------------------------------
|
|
108
|
+
|
|
109
|
+
export interface HttpResponseLike {
|
|
110
|
+
status: number;
|
|
111
|
+
json(): Promise<unknown>;
|
|
112
|
+
}
|
|
113
|
+
export interface HttpTransport {
|
|
114
|
+
post(url: string, headers: Record<string, string>, body: unknown): Promise<HttpResponseLike>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Default transport: a JSON `fetch` POST. */
|
|
118
|
+
export class FetchHttpTransport implements HttpTransport {
|
|
119
|
+
async post(url: string, headers: Record<string, string>, body: unknown) {
|
|
120
|
+
return fetch(url, {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: { "content-type": "application/json", accept: "application/json", ...headers },
|
|
123
|
+
body: JSON.stringify(body),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface HttpTotAccessClientConfig {
|
|
129
|
+
/** tot20 core API base (no trailing slash), e.g. https://qa.tokenoftrust.com */
|
|
130
|
+
baseUrl: string;
|
|
131
|
+
/** Storefront's own ToT credentials (machine auth to the private endpoints). */
|
|
132
|
+
apiKey: string;
|
|
133
|
+
secretKey?: string;
|
|
134
|
+
transport?: HttpTransport;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export class TotAccessClientError extends Error {}
|
|
138
|
+
|
|
139
|
+
/** The real client. Neutral/typed on failure — a gate must fail closed, not throw. */
|
|
140
|
+
export class HttpTotAccessClient implements TotAccessClient {
|
|
141
|
+
private readonly baseUrl: string;
|
|
142
|
+
private readonly apiKey: string;
|
|
143
|
+
private readonly secretKey?: string;
|
|
144
|
+
private readonly transport: HttpTransport;
|
|
145
|
+
|
|
146
|
+
constructor(config: HttpTotAccessClientConfig) {
|
|
147
|
+
if (!config.baseUrl) throw new TotAccessClientError("access: TOT_CORE_URL is not configured");
|
|
148
|
+
if (!config.apiKey) throw new TotAccessClientError("access: storefront ToT credentials are not configured");
|
|
149
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
150
|
+
this.apiKey = config.apiKey;
|
|
151
|
+
this.secretKey = config.secretKey;
|
|
152
|
+
this.transport = config.transport ?? new FetchHttpTransport();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The one integration-sensitive spot: how the storefront authenticates to the
|
|
157
|
+
* `requireSecretKey` endpoints. tot20 reads `totApiKey`/`totSecretKey` from the
|
|
158
|
+
* request body (`normalizeSecureUserRequest`), so we merge the storefront's own
|
|
159
|
+
* credentials into every POST body. Swap for a machine-JWT `Authorization:
|
|
160
|
+
* Bearer` here if tot20 ever requires it — this is the only place to change.
|
|
161
|
+
*/
|
|
162
|
+
private credentialFields(): Record<string, string> {
|
|
163
|
+
const f: Record<string, string> = { totApiKey: this.apiKey };
|
|
164
|
+
if (this.secretKey) f.totSecretKey = this.secretKey;
|
|
165
|
+
return f;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async sendAccessCode(email: string, resource: string): Promise<AccessCodeSendResult> {
|
|
169
|
+
// Neutral by contract: even a transport error resolves to { ok: true } so the
|
|
170
|
+
// UI never leaks eligibility. (Real failures are logged server-side upstream.)
|
|
171
|
+
try {
|
|
172
|
+
await this.transport.post(
|
|
173
|
+
`${this.baseUrl}/api/internal/access-code/send`,
|
|
174
|
+
{},
|
|
175
|
+
{ email: email.toLowerCase(), resource, ...this.credentialFields() },
|
|
176
|
+
);
|
|
177
|
+
} catch {
|
|
178
|
+
/* swallow — neutral response */
|
|
179
|
+
}
|
|
180
|
+
return { ok: true };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async verifyAccessCode(
|
|
184
|
+
email: string,
|
|
185
|
+
resource: string,
|
|
186
|
+
code: string,
|
|
187
|
+
): Promise<AccessCodeVerifyResult> {
|
|
188
|
+
let res: HttpResponseLike;
|
|
189
|
+
try {
|
|
190
|
+
res = await this.transport.post(
|
|
191
|
+
`${this.baseUrl}/api/internal/access-code/verify`,
|
|
192
|
+
{},
|
|
193
|
+
{ email: email.toLowerCase(), resource, code, ...this.credentialFields() },
|
|
194
|
+
);
|
|
195
|
+
} catch {
|
|
196
|
+
return { ok: false, reason: "error" };
|
|
197
|
+
}
|
|
198
|
+
let body: unknown;
|
|
199
|
+
try {
|
|
200
|
+
body = await res.json();
|
|
201
|
+
} catch {
|
|
202
|
+
return { ok: false, reason: "error" };
|
|
203
|
+
}
|
|
204
|
+
return normalizeVerify(body, email, resource);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async sendFederatedCode(email: string): Promise<AccessCodeSendResult> {
|
|
208
|
+
// Neutral by contract: a transport error still resolves to { ok: true } so the UI
|
|
209
|
+
// never leaks eligibility. But SERVER-SIDE we log tot20's real status/body — the
|
|
210
|
+
// client never sees it (enumeration-resistance preserved), and it's the only way
|
|
211
|
+
// to see why a code didn't send. Part of the cross-repo email-send trace.
|
|
212
|
+
try {
|
|
213
|
+
const res = await this.transport.post(
|
|
214
|
+
`${this.baseUrl}/api/internal/access-code/federated-send`,
|
|
215
|
+
{},
|
|
216
|
+
{ email: email.toLowerCase(), ...this.credentialFields() },
|
|
217
|
+
);
|
|
218
|
+
let body: unknown = "(unread)";
|
|
219
|
+
try {
|
|
220
|
+
body = await res.json();
|
|
221
|
+
} catch {
|
|
222
|
+
body = "(non-JSON)";
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
console.info(
|
|
226
|
+
"[diag:federated-send]",
|
|
227
|
+
JSON.stringify({ email: diagEmail(email.toLowerCase()), status: res.status, body }),
|
|
228
|
+
);
|
|
229
|
+
} catch {
|
|
230
|
+
/* never break */
|
|
231
|
+
}
|
|
232
|
+
} catch (e) {
|
|
233
|
+
try {
|
|
234
|
+
console.warn(
|
|
235
|
+
"[diag:federated-send] transport error",
|
|
236
|
+
e instanceof Error ? e.message : String(e),
|
|
237
|
+
);
|
|
238
|
+
} catch {
|
|
239
|
+
/* never break */
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { ok: true };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async verifyFederatedCode(
|
|
246
|
+
email: string,
|
|
247
|
+
code: string,
|
|
248
|
+
): Promise<FederatedVerifyResult> {
|
|
249
|
+
let res: HttpResponseLike;
|
|
250
|
+
try {
|
|
251
|
+
res = await this.transport.post(
|
|
252
|
+
`${this.baseUrl}/api/internal/access-code/federated-verify`,
|
|
253
|
+
{},
|
|
254
|
+
{ email: email.toLowerCase(), code, ...this.credentialFields() },
|
|
255
|
+
);
|
|
256
|
+
} catch {
|
|
257
|
+
return { ok: false, reason: "error" };
|
|
258
|
+
}
|
|
259
|
+
let body: unknown;
|
|
260
|
+
try {
|
|
261
|
+
body = await res.json();
|
|
262
|
+
} catch {
|
|
263
|
+
return { ok: false, reason: "error" };
|
|
264
|
+
}
|
|
265
|
+
return normalizeFederatedVerify(body, email);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async exchangeSignInToken(token: string): Promise<FederatedVerifyResult> {
|
|
269
|
+
let res: HttpResponseLike;
|
|
270
|
+
try {
|
|
271
|
+
res = await this.transport.post(
|
|
272
|
+
`${this.baseUrl}/api/internal/access-code/exchange`,
|
|
273
|
+
{},
|
|
274
|
+
{ token, ...this.credentialFields() },
|
|
275
|
+
);
|
|
276
|
+
} catch {
|
|
277
|
+
return { ok: false, reason: "error" };
|
|
278
|
+
}
|
|
279
|
+
let body: unknown;
|
|
280
|
+
try {
|
|
281
|
+
body = await res.json();
|
|
282
|
+
} catch {
|
|
283
|
+
return { ok: false, reason: "error" };
|
|
284
|
+
}
|
|
285
|
+
// The token is bound to (email, audience) server-side; the response carries the
|
|
286
|
+
// authoritative email (no client-supplied fallback needed).
|
|
287
|
+
return normalizeFederatedVerify(body, "");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Parse tot20's FEDERATED verify response into a typed result (fail closed). */
|
|
292
|
+
export function normalizeFederatedVerify(
|
|
293
|
+
body: unknown,
|
|
294
|
+
email: string,
|
|
295
|
+
): FederatedVerifyResult {
|
|
296
|
+
// Unwrap the `{ content, metadata }` envelope when present (see normalizeVerify).
|
|
297
|
+
const raw = (body ?? {}) as Record<string, unknown>;
|
|
298
|
+
const b = (raw.content && typeof raw.content === "object" ? raw.content : raw) as Record<string, unknown>;
|
|
299
|
+
if (b.ok === true) {
|
|
300
|
+
const idToken =
|
|
301
|
+
typeof b.id_token === "string"
|
|
302
|
+
? b.id_token
|
|
303
|
+
: typeof b.assertion === "string"
|
|
304
|
+
? b.assertion
|
|
305
|
+
: undefined;
|
|
306
|
+
const result: FederatedVerifyResult = {
|
|
307
|
+
ok: true,
|
|
308
|
+
email: typeof b.email === "string" ? b.email.toLowerCase() : email.toLowerCase(),
|
|
309
|
+
audience: typeof b.audience === "string" ? b.audience : "",
|
|
310
|
+
};
|
|
311
|
+
if (idToken) result.id_token = idToken;
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
314
|
+
const reason = b.reason;
|
|
315
|
+
const known: VerifyFailureReason[] = ["invalid", "expired", "too_many_attempts", "error"];
|
|
316
|
+
return {
|
|
317
|
+
ok: false,
|
|
318
|
+
reason: known.includes(reason as VerifyFailureReason) ? (reason as VerifyFailureReason) : "invalid",
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Parse + validate tot20's verify response into our typed result (fail closed). */
|
|
323
|
+
export function normalizeVerify(
|
|
324
|
+
body: unknown,
|
|
325
|
+
email: string,
|
|
326
|
+
resource: string,
|
|
327
|
+
): AccessCodeVerifyResult {
|
|
328
|
+
// tot20 wraps every API response in a `{ content, metadata }` envelope
|
|
329
|
+
// (swagger `sendJsonResponse`). Verified against live qa 2026-07-05: access-code
|
|
330
|
+
// /verify returns `{ content: { ok, email, resource, id_token }, metadata }`.
|
|
331
|
+
// Unwrap `content` when present; fall back to the raw body (the Mock + tests
|
|
332
|
+
// return the un-enveloped shape).
|
|
333
|
+
const raw = (body ?? {}) as Record<string, unknown>;
|
|
334
|
+
const b = (raw.content && typeof raw.content === "object" ? raw.content : raw) as Record<string, unknown>;
|
|
335
|
+
// Success is signalled by `ok === true` alone. tot20 DROPPED the unsigned
|
|
336
|
+
// `roles`/`capability` fields (contract §6, C2/R1) — the signed `id_token` is
|
|
337
|
+
// now the SOLE authority, and the storefront derives identity/membership from
|
|
338
|
+
// its verified claims (see auth/verify.ts), never from these echoes. So do NOT
|
|
339
|
+
// gate success on `roles` being present. `roles`/`capability` remain optional
|
|
340
|
+
// (populated only if a legacy/mock response still carries them).
|
|
341
|
+
if (b.ok === true) {
|
|
342
|
+
// TRANSITIONAL: accept `assertion` as well as `id_token` — some tot builds
|
|
343
|
+
// emitted the pre-rename field name. Contract §6 fixes it as `id_token`.
|
|
344
|
+
const idToken = typeof b.id_token === "string" ? b.id_token : typeof b.assertion === "string" ? b.assertion : undefined;
|
|
345
|
+
const result: AccessCodeVerifyResult = {
|
|
346
|
+
ok: true,
|
|
347
|
+
email: typeof b.email === "string" ? b.email.toLowerCase() : email.toLowerCase(),
|
|
348
|
+
resource: typeof b.resource === "string" ? b.resource : resource,
|
|
349
|
+
roles: Array.isArray(b.roles) ? b.roles.filter((r): r is string => typeof r === "string") : [],
|
|
350
|
+
capability: typeof b.capability === "string" ? b.capability : "none",
|
|
351
|
+
};
|
|
352
|
+
// Only attach when present so consumers can distinguish "signer degraded"
|
|
353
|
+
// from "no field", and so existing exact-shape tests stay green.
|
|
354
|
+
if (idToken) result.id_token = idToken;
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
const reason = b.reason;
|
|
358
|
+
const known: VerifyFailureReason[] = ["invalid", "expired", "too_many_attempts", "error"];
|
|
359
|
+
return {
|
|
360
|
+
ok: false,
|
|
361
|
+
reason: known.includes(reason as VerifyFailureReason) ? (reason as VerifyFailureReason) : "invalid",
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// --- Mock (tests + local dev before tot20 lands) ----------------------------
|
|
366
|
+
|
|
367
|
+
export interface MockRosterEntry {
|
|
368
|
+
roles: string[];
|
|
369
|
+
/** Single effective capability; derived if omitted. */
|
|
370
|
+
capability?: string;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* In-memory TotAccessClient. Seeded with a per-(resource,email) roster and a
|
|
375
|
+
* fixed code, it drives route + gate tests deterministically and lets local dev
|
|
376
|
+
* exercise the flow before the tot20 endpoints exist.
|
|
377
|
+
*/
|
|
378
|
+
export class MockTotAccessClient implements TotAccessClient {
|
|
379
|
+
/** key = `${resource}${emailLower}` → roster entry. */
|
|
380
|
+
private roster = new Map<string, MockRosterEntry>();
|
|
381
|
+
sent: Array<{ email: string; resource: string }> = [];
|
|
382
|
+
|
|
383
|
+
constructor(
|
|
384
|
+
private readonly code = "123456",
|
|
385
|
+
seed: Array<{ email: string; resource: string; roles: string[]; capability?: string }> = [],
|
|
386
|
+
/**
|
|
387
|
+
* Optional signer: when set, `verifyAccessCode` also emits an `id_token` so
|
|
388
|
+
* the mock mirrors tot's Phase-3 signed assertion (contract §6). Left unset
|
|
389
|
+
* by default, keeping the flat-only shape existing tests assert on. Wire a
|
|
390
|
+
* real ES256 signer in dev/integration to exercise offline verify end-to-end.
|
|
391
|
+
*/
|
|
392
|
+
private readonly signIdToken?: (input: {
|
|
393
|
+
email: string;
|
|
394
|
+
resource: string;
|
|
395
|
+
roles: string[];
|
|
396
|
+
capability: string;
|
|
397
|
+
}) => Promise<string>,
|
|
398
|
+
) {
|
|
399
|
+
for (const s of seed) this.grant(s.email, s.resource, s.roles, s.capability);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
grant(email: string, resource: string, roles: string[], capability?: string) {
|
|
403
|
+
this.roster.set(this.key(resource, email), { roles, capability });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private key(resource: string, email: string) {
|
|
407
|
+
return `${resource}${email.toLowerCase()}`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async sendAccessCode(email: string, resource: string): Promise<AccessCodeSendResult> {
|
|
411
|
+
this.sent.push({ email: email.toLowerCase(), resource });
|
|
412
|
+
return { ok: true }; // neutral regardless of eligibility
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async verifyAccessCode(
|
|
416
|
+
email: string,
|
|
417
|
+
resource: string,
|
|
418
|
+
code: string,
|
|
419
|
+
): Promise<AccessCodeVerifyResult> {
|
|
420
|
+
if (code !== this.code) return { ok: false, reason: "invalid" };
|
|
421
|
+
const entry = this.roster.get(this.key(resource, email));
|
|
422
|
+
if (!entry) return { ok: false, reason: "invalid" }; // not eligible → indistinguishable
|
|
423
|
+
const capability = entry.capability ?? effectiveCapability(entry.roles);
|
|
424
|
+
const result: AccessCodeVerifyResult = {
|
|
425
|
+
ok: true,
|
|
426
|
+
email: email.toLowerCase(),
|
|
427
|
+
resource,
|
|
428
|
+
roles: entry.roles,
|
|
429
|
+
capability,
|
|
430
|
+
};
|
|
431
|
+
if (this.signIdToken) {
|
|
432
|
+
result.id_token = await this.signIdToken({
|
|
433
|
+
email: email.toLowerCase(),
|
|
434
|
+
resource,
|
|
435
|
+
roles: entry.roles,
|
|
436
|
+
capability,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
return result;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async sendFederatedCode(email: string): Promise<AccessCodeSendResult> {
|
|
443
|
+
this.sent.push({ email: email.toLowerCase(), resource: "*" }); // federated: no single resource
|
|
444
|
+
return { ok: true };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async verifyFederatedCode(
|
|
448
|
+
email: string,
|
|
449
|
+
code: string,
|
|
450
|
+
): Promise<FederatedVerifyResult> {
|
|
451
|
+
if (code !== this.code) return { ok: false, reason: "invalid" };
|
|
452
|
+
// Eligible if the email holds a role on ANY seeded resource. Roster keys are
|
|
453
|
+
// `${resource}<sep>${emailLower}`; match on the email suffix (an email's `@`
|
|
454
|
+
// never appears in an appDomain resource, so this can't false-match).
|
|
455
|
+
const lower = email.toLowerCase();
|
|
456
|
+
const hasAny = [...this.roster.keys()].some((k) => k.endsWith(lower));
|
|
457
|
+
if (!hasAny) return { ok: false, reason: "invalid" };
|
|
458
|
+
// The multi-tenant id_token is minted by tot in production; the mock returns
|
|
459
|
+
// the shape without a token unless a signer is wired (tests inject one).
|
|
460
|
+
return { ok: true, email: lower, audience: "storefront" };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async exchangeSignInToken(token: string): Promise<FederatedVerifyResult> {
|
|
464
|
+
// Accept the mock tokens minted by MockDeveloperInviteClient ("mock|<tenant>|<email>").
|
|
465
|
+
try {
|
|
466
|
+
const [tag, , email] = Buffer.from(token, "base64url").toString("utf8").split("|");
|
|
467
|
+
if (tag === "mock" && email) {
|
|
468
|
+
return { ok: true, email: email.toLowerCase(), audience: "storefront" };
|
|
469
|
+
}
|
|
470
|
+
} catch {
|
|
471
|
+
/* fall through to invalid */
|
|
472
|
+
}
|
|
473
|
+
return { ok: false, reason: "invalid" };
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Collapse a role array to one effective capability (mirrors tot20's precedence). */
|
|
478
|
+
export function effectiveCapability(roles: readonly string[]): string {
|
|
479
|
+
const has = (r: string) => roles.includes(r);
|
|
480
|
+
if (has("apikey:owner") || has("appOwner")) return "owner";
|
|
481
|
+
if (has("appAdmin")) return "admin";
|
|
482
|
+
if (has("developer")) return "developer";
|
|
483
|
+
if (has("inviter")) return "inviter";
|
|
484
|
+
if (has("administrator") || has("super-admin") || has("support")) return "staff";
|
|
485
|
+
return roles[0] ?? "none";
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// --- Factory ----------------------------------------------------------------
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Build the env-configured client:
|
|
492
|
+
* - `TOT_CORE_URL` — tot20 core API base.
|
|
493
|
+
* - `TOT_API_KEY` (+ optional `TOT_SECRET_KEY`) — storefront's own credentials.
|
|
494
|
+
* Throws {@link TotAccessClientError} when unconfigured (routes map that to 503).
|
|
495
|
+
*/
|
|
496
|
+
export async function createTotAccessClient(
|
|
497
|
+
overrides: Partial<HttpTotAccessClientConfig> = {},
|
|
498
|
+
): Promise<HttpTotAccessClient> {
|
|
499
|
+
const baseUrl = overrides.baseUrl ?? (await readEnv("TOT_CORE_URL")) ?? "";
|
|
500
|
+
const apiKey = overrides.apiKey ?? (await readEnv("TOT_API_KEY")) ?? "";
|
|
501
|
+
const secretKey = overrides.secretKey ?? (await readEnv("TOT_SECRET_KEY"));
|
|
502
|
+
return new HttpTotAccessClient({ ...overrides, baseUrl, apiKey, secretKey });
|
|
503
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant base-path helpers.
|
|
3
|
+
*
|
|
4
|
+
* When a request selects its tenant via a `/<domain>/` path prefix (see
|
|
5
|
+
* lib/tenantRouting.ts), every same-origin link must carry that prefix or the
|
|
6
|
+
* visitor falls out of the tenant. Host-routed requests have an empty base path
|
|
7
|
+
* and links are returned unchanged.
|
|
8
|
+
*
|
|
9
|
+
* `withBase` is the single source of truth for that rule and is used both
|
|
10
|
+
* - server-side, from `Astro.locals.basePath`, in .astro components, and
|
|
11
|
+
* - client-side, from `clientBasePath()` (the <html data-base-path> attr set
|
|
12
|
+
* in Layout.astro), in hydrated islands.
|
|
13
|
+
*
|
|
14
|
+
* It is idempotent and conservative: only root-absolute single-slash paths are
|
|
15
|
+
* prefixed. Protocol-relative URLs ("//cdn…"), absolute URLs ("https://…"),
|
|
16
|
+
* fragments/queries ("#x", "?x"), and already-prefixed paths pass through. This
|
|
17
|
+
* mirrors the defense-in-depth client script in Layout.astro.
|
|
18
|
+
*/
|
|
19
|
+
export function withBase(basePath: string, href: string): string {
|
|
20
|
+
if (!basePath || !href) return href;
|
|
21
|
+
// Only same-origin root-absolute paths; skip protocol-relative "//".
|
|
22
|
+
if (href.charAt(0) !== "/" || href.charAt(1) === "/") return href;
|
|
23
|
+
// Already inside the prefix.
|
|
24
|
+
if (href === basePath || href.startsWith(`${basePath}/`)) return href;
|
|
25
|
+
return `${basePath}${href}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The active tenant base path as seen from the browser, read from the
|
|
30
|
+
* `<html data-base-path>` attribute Layout.astro emits. Returns "" on the
|
|
31
|
+
* server, for host-routed tenants, or before hydration.
|
|
32
|
+
*/
|
|
33
|
+
export function clientBasePath(): string {
|
|
34
|
+
if (typeof document === "undefined") return "";
|
|
35
|
+
return document.documentElement.dataset.basePath ?? "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Convenience for hydrated islands: prefix `href` with the client base path. */
|
|
39
|
+
export function withClientBase(href: string): string {
|
|
40
|
+
return withBase(clientBasePath(), href);
|
|
41
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Image Resizing URL helpers (Epic Phase-2 D2).
|
|
3
|
+
*
|
|
4
|
+
* Rehosted product images live in the PRODUCT_IMAGES R2 bucket, which does NOT
|
|
5
|
+
* transform on a `?width=` query the way Shopify's CDN does. Cloudflare Image
|
|
6
|
+
* Resizing — the `/cdn-cgi/image/<options>/<source>` path, run at the zone edge —
|
|
7
|
+
* resizes AND reformats (`format=auto` → AVIF/WebP where supported) any same-zone
|
|
8
|
+
* or allowlisted source.
|
|
9
|
+
*
|
|
10
|
+
* We emit ROOT-RELATIVE `/cdn-cgi/image/...` URLs so they resolve against
|
|
11
|
+
* whichever zone is serving the page (and an extracted/self-hosted tenant's zone
|
|
12
|
+
* too — no hardcoded host). Referenced (non-rehosted) Shopify CDN URLs keep their
|
|
13
|
+
* native `?width=` scheme in Img.astro, since Shopify already resizes those — no
|
|
14
|
+
* need to route them back through our resizer.
|
|
15
|
+
*/
|
|
16
|
+
export interface CfImageOpts {
|
|
17
|
+
width: number;
|
|
18
|
+
height?: number;
|
|
19
|
+
fit?: "cover" | "contain" | "scale-down" | "crop" | "pad";
|
|
20
|
+
format?: "auto" | "avif" | "webp";
|
|
21
|
+
quality?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The responsive widths we generate, in CSS px. */
|
|
25
|
+
export const CF_IMAGE_WIDTHS = [320, 480, 640, 960, 1280, 1600] as const;
|
|
26
|
+
|
|
27
|
+
/** Build a single `/cdn-cgi/image/...` URL for a source image. */
|
|
28
|
+
export function cfImageUrl(src: string, opts: CfImageOpts): string {
|
|
29
|
+
const options = [
|
|
30
|
+
`width=${Math.round(opts.width)}`,
|
|
31
|
+
opts.height ? `height=${Math.round(opts.height)}` : null,
|
|
32
|
+
`fit=${opts.fit ?? "cover"}`,
|
|
33
|
+
`format=${opts.format ?? "auto"}`,
|
|
34
|
+
`quality=${opts.quality ?? 82}`,
|
|
35
|
+
]
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.join(",");
|
|
38
|
+
// Cloudflare accepts both absolute source URLs and same-zone paths verbatim
|
|
39
|
+
// after the options segment.
|
|
40
|
+
return `/cdn-cgi/image/${options}/${src}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build a `srcset` of resized variants up to ~2x the display width, so the
|
|
45
|
+
* browser picks the right density. `displayWidth` is the intrinsic/layout width.
|
|
46
|
+
*/
|
|
47
|
+
export function cfImageSrcset(
|
|
48
|
+
src: string,
|
|
49
|
+
displayWidth: number,
|
|
50
|
+
opts: Omit<CfImageOpts, "width"> = {},
|
|
51
|
+
): string {
|
|
52
|
+
const widths = CF_IMAGE_WIDTHS.filter((w) => w <= displayWidth * 2);
|
|
53
|
+
const list = widths.length ? widths : [displayWidth];
|
|
54
|
+
return list.map((w) => `${cfImageUrl(src, { ...opts, width: w })} ${w}w`).join(", ");
|
|
55
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome model — typed props for the reusable, tenant-agnostic site-chrome
|
|
3
|
+
* components (AnnouncementBar, UtilityNav, SiteHeader, NavDropdown/MegaMenu,
|
|
4
|
+
* SiteFooter). Chrome is a STANDARD component set skinned by theme tokens, never
|
|
5
|
+
* hand-built per tenant page. A tenant's chrome is data (`chrome.json`) that
|
|
6
|
+
* populates these shapes; the wrapping mechanism (see lib/rawChrome.ts) surrounds
|
|
7
|
+
* body-only page fragments with the rendered chrome.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** A navigation/footer link. `external` opens off-site; `badge` shows e.g. "NEW". */
|
|
11
|
+
export interface ChromeLink {
|
|
12
|
+
label: string;
|
|
13
|
+
href: string;
|
|
14
|
+
external?: boolean;
|
|
15
|
+
badge?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** One column of a mega-menu panel. */
|
|
19
|
+
export interface MegaColumn {
|
|
20
|
+
title?: string;
|
|
21
|
+
links: ChromeLink[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A primary-nav item. A plain link (no children/columns), a single-column
|
|
26
|
+
* dropdown (`children`), or a multi-column mega-menu (`columns`). `badge` marks
|
|
27
|
+
* it (e.g. "NEW"); `flag` gives it the emphasized/highlighted treatment.
|
|
28
|
+
*/
|
|
29
|
+
export interface ChromeNavItem {
|
|
30
|
+
label: string;
|
|
31
|
+
href: string;
|
|
32
|
+
badge?: string;
|
|
33
|
+
flag?: boolean;
|
|
34
|
+
children?: ChromeLink[];
|
|
35
|
+
columns?: MegaColumn[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SocialLink {
|
|
39
|
+
label: string;
|
|
40
|
+
href: string;
|
|
41
|
+
/** Optional inline-SVG path data (single <path d="…">) — no external images. */
|
|
42
|
+
iconPath?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ChromeLogo {
|
|
46
|
+
src: string;
|
|
47
|
+
alt: string;
|
|
48
|
+
width?: number;
|
|
49
|
+
height?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface FooterColumn {
|
|
53
|
+
title: string;
|
|
54
|
+
links: ChromeLink[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface FooterNewsletter {
|
|
58
|
+
title?: string;
|
|
59
|
+
body?: string;
|
|
60
|
+
/** Form action (defaults to /api/newsletter). */
|
|
61
|
+
action?: string;
|
|
62
|
+
placeholder?: string;
|
|
63
|
+
/** Hidden `source` field value (e.g. "footer"). */
|
|
64
|
+
source?: string;
|
|
65
|
+
}
|