@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,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET|POST /api/dashboard/vendor-search?q=<prefix> — the ToT-STAFF vendor search
|
|
3
|
+
* proxy (#8425 staff access). Phase 1 of two-phase staff admission: a staff viewer
|
|
4
|
+
* finds a vendor by prefix; there is no silent access to every host.
|
|
5
|
+
*
|
|
6
|
+
* AUTHORIZATION — two independent axes:
|
|
7
|
+
* NEAR (this endpoint): a valid `tot_session` that carries ToT-STAFF scope
|
|
8
|
+
* (`staff[]` non-empty). No session → 401; a session without staff scope →
|
|
9
|
+
* 403 (a plain member has nothing to search across — they use their picker).
|
|
10
|
+
* FAR (tot20): storefront calls `/clients/vendorSearch` as its OWN operator
|
|
11
|
+
* identity, presenting a storefront-signed staff broker assertion in the
|
|
12
|
+
* `X-ToT-Broker-Assertion` header (minted from the verified session email);
|
|
13
|
+
* tot20 verifies it, derives the staff actor from its `sub`, and scopes results
|
|
14
|
+
* to that user's staffRoles. No spoofable plaintext email crosses the wire.
|
|
15
|
+
*
|
|
16
|
+
* GET reads `q` (+ optional `limit`) from the query; POST reads them from a JSON
|
|
17
|
+
* body — the autocomplete client uses GET, but POST is accepted so a no-JS/form
|
|
18
|
+
* path works too. Returns `{ vendors: [{ appDomain, displayName }] }`.
|
|
19
|
+
*/
|
|
20
|
+
import type { APIContext } from "astro";
|
|
21
|
+
import { readViewerSession } from "@/lib/auth/route";
|
|
22
|
+
import { isStaffSession } from "@/lib/dashboard/staffAdmission";
|
|
23
|
+
import {
|
|
24
|
+
createStaffVendorAccessClient,
|
|
25
|
+
staffAccessHttpStatus,
|
|
26
|
+
StaffVendorAccessClientError,
|
|
27
|
+
} from "@/lib/dashboard/staffVendorAccess";
|
|
28
|
+
import { mintBrokerAssertion, BrokerAssertionError } from "@/lib/auth/brokerAssertion";
|
|
29
|
+
import {
|
|
30
|
+
parseVendorQuery,
|
|
31
|
+
ANCHORED_FETCH_LIMIT,
|
|
32
|
+
ANCHORED_DISPLAY_LIMIT,
|
|
33
|
+
} from "@/lib/dashboard/vendorQuery";
|
|
34
|
+
|
|
35
|
+
export const prerender = false;
|
|
36
|
+
|
|
37
|
+
/** JSON response with no-store (staff access state must never be edge-cached). */
|
|
38
|
+
function json(body: unknown, status = 200): Response {
|
|
39
|
+
return new Response(JSON.stringify(body), {
|
|
40
|
+
status,
|
|
41
|
+
headers: { "content-type": "application/json", "cache-control": "no-store" },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function handle(context: APIContext, q: string, limit: number | undefined): Promise<Response> {
|
|
46
|
+
const session = await readViewerSession(context);
|
|
47
|
+
if (!session) return json({ error: "not signed in" }, 401);
|
|
48
|
+
if (!isStaffSession(session)) {
|
|
49
|
+
return json({ error: "staff access required", reason: "notStaff" }, 403);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let client;
|
|
53
|
+
try {
|
|
54
|
+
client = await createStaffVendorAccessClient();
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (err instanceof StaffVendorAccessClientError) {
|
|
57
|
+
return json({ error: "staff-access integration not configured" }, 503);
|
|
58
|
+
}
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// #8453 `staff-invite-proof-via-broker`: prove the staff actor with a fresh
|
|
63
|
+
// storefront-signed assertion minted from the VERIFIED session email — tot20
|
|
64
|
+
// derives the actor + scopes results from it. Fail closed (503) if unconfigured.
|
|
65
|
+
let brokerAssertion: string;
|
|
66
|
+
try {
|
|
67
|
+
brokerAssertion = await mintBrokerAssertion(session.email);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (err instanceof BrokerAssertionError) {
|
|
70
|
+
return json({ error: "staff-proof signing not configured" }, 503);
|
|
71
|
+
}
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Anchor syntax (^prefix / suffix$ / ^exact$) narrows tot20's substring search.
|
|
76
|
+
// Send the anchor-stripped CORE term upstream (with a widened cap so the exact hit
|
|
77
|
+
// is in the set), then post-filter by the anchor against appDomain. A plain query
|
|
78
|
+
// is unchanged: sent as-is, no post-filter. See @/lib/dashboard/vendorQuery.
|
|
79
|
+
const parsed = parseVendorQuery(q);
|
|
80
|
+
const upstreamQuery = parsed.anchored ? parsed.core : q;
|
|
81
|
+
const upstreamLimit = parsed.anchored ? ANCHORED_FETCH_LIMIT : limit;
|
|
82
|
+
|
|
83
|
+
const result = await client.searchVendors(brokerAssertion, upstreamQuery, upstreamLimit);
|
|
84
|
+
if (!result.ok) {
|
|
85
|
+
return json(
|
|
86
|
+
{ error: result.message, reason: result.reason, code: result.code },
|
|
87
|
+
staffAccessHttpStatus(result.reason, result.status),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const vendors = parsed.anchored
|
|
91
|
+
? result.vendors
|
|
92
|
+
.filter((v) => parsed.matches(v.appDomain))
|
|
93
|
+
.slice(0, limit && limit > 0 ? limit : ANCHORED_DISPLAY_LIMIT)
|
|
94
|
+
: result.vendors;
|
|
95
|
+
return json({ vendors });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseLimit(raw: string | null | undefined): number | undefined {
|
|
99
|
+
if (raw == null || raw === "") return undefined;
|
|
100
|
+
const n = Number(raw);
|
|
101
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
105
|
+
const url = new URL(context.request.url);
|
|
106
|
+
const q = (url.searchParams.get("q") ?? "").trim();
|
|
107
|
+
return handle(context, q, parseLimit(url.searchParams.get("limit")));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
111
|
+
let body: { q?: unknown; limit?: unknown };
|
|
112
|
+
try {
|
|
113
|
+
body = (await context.request.json()) as typeof body;
|
|
114
|
+
} catch {
|
|
115
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
116
|
+
}
|
|
117
|
+
const q = typeof body.q === "string" ? body.q.trim() : "";
|
|
118
|
+
const limit = typeof body.limit === "number" ? body.limit : undefined;
|
|
119
|
+
return handle(context, q, limit);
|
|
120
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/dev/cli-signin-code — mint a browserless CLI sign-in command for the
|
|
3
|
+
* SIGNED-IN developer (DevAccess #8425). The /dev getting-started page calls this to
|
|
4
|
+
* render a copyable `tot login --code <token>` so the developer's terminal signs in with
|
|
5
|
+
* no second browser round-trip and no OTP.
|
|
6
|
+
*
|
|
7
|
+
* AUTHORIZATION: the viewer's own session (the `tot_session` cookie). 401 if not signed
|
|
8
|
+
* in, 403 if the session carries no developer capability (nothing to sign a CLI into).
|
|
9
|
+
* The minted-for email is the VERIFIED session email — NEVER read from the body — so a
|
|
10
|
+
* developer can only mint a code for themselves.
|
|
11
|
+
*
|
|
12
|
+
* The response `command` CONTAINS the single-use token; it is returned only to this
|
|
13
|
+
* authenticated developer's own browser and is never logged server-side.
|
|
14
|
+
*/
|
|
15
|
+
import type { APIContext } from "astro";
|
|
16
|
+
import { readViewerSession } from "@/lib/auth/route";
|
|
17
|
+
import { mintCliSignInCommand } from "@/lib/dev/cliSignInCode";
|
|
18
|
+
|
|
19
|
+
export const prerender = false;
|
|
20
|
+
|
|
21
|
+
function json(body: unknown, status = 200): Response {
|
|
22
|
+
return new Response(JSON.stringify(body), {
|
|
23
|
+
status,
|
|
24
|
+
headers: { "content-type": "application/json" },
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
29
|
+
const session = await readViewerSession(context);
|
|
30
|
+
if (!session) return json({ error: "not signed in" }, 401);
|
|
31
|
+
// Same gate the /dev page applies (canUseAI): a real developer capability. A
|
|
32
|
+
// no-capability session has no scoped CLI session to mint.
|
|
33
|
+
if (!session.capability || session.capability === "none") {
|
|
34
|
+
return json({ error: "no developer access on this session", reason: "notEligible" }, 403);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = await mintCliSignInCommand(session.email);
|
|
38
|
+
if (!result.ok) {
|
|
39
|
+
// 503 = seam not configured / MCP route unmounted; 502 = MCP fault. Surface neither
|
|
40
|
+
// the token nor internal detail beyond a terse reason.
|
|
41
|
+
return json({ error: "CLI sign-in code unavailable", reason: result.reason }, result.status);
|
|
42
|
+
}
|
|
43
|
+
return json({ command: result.command, expiresAt: result.expiresAt });
|
|
44
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newsletter signup endpoint.
|
|
3
|
+
*
|
|
4
|
+
* ASSUMPTION (Phase 1): there is no ESP integration yet. We persist the email to
|
|
5
|
+
* the TENANT_CACHE KV namespace when the Cloudflare runtime binding is present
|
|
6
|
+
* (key `newsletter:<email>`), and otherwise just log. Wiring a real ESP
|
|
7
|
+
* (Klaviyo/Mailchimp) is a documented Phase 2 swap at the marked spot below.
|
|
8
|
+
*
|
|
9
|
+
* Robustness: a signup must NEVER 500. All failures degrade to a friendly
|
|
10
|
+
* thank-you so the visitor is never shown an error for handing us their email.
|
|
11
|
+
*/
|
|
12
|
+
import type { APIContext } from "astro";
|
|
13
|
+
import { readKv } from "@/lib/env";
|
|
14
|
+
|
|
15
|
+
// Minimal, permissive email shape check — we are not the source of truth for
|
|
16
|
+
// deliverability; the ESP validates properly downstream.
|
|
17
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
18
|
+
|
|
19
|
+
function thankYou(message: string, status = 200): Response {
|
|
20
|
+
const 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" />
|
|
26
|
+
<title>Thank you</title>
|
|
27
|
+
<style>
|
|
28
|
+
:root { color-scheme: light dark; }
|
|
29
|
+
body { font-family: ui-serif, Georgia, serif; margin: 0; min-height: 100vh;
|
|
30
|
+
display: grid; place-items: center; padding: 2rem; text-align: center;
|
|
31
|
+
background: #f4f1ea; color: #2f3a34; }
|
|
32
|
+
.card { max-width: 30rem; }
|
|
33
|
+
h1 { font-size: clamp(1.6rem, 4vw, 2.2rem); margin: 0 0 0.75rem; }
|
|
34
|
+
p { color: #5a655e; line-height: 1.6; }
|
|
35
|
+
a { color: #2f3a34; font-weight: 600; }
|
|
36
|
+
</style>
|
|
37
|
+
</head>
|
|
38
|
+
<body>
|
|
39
|
+
<div class="card">
|
|
40
|
+
<h1>You're on the list.</h1>
|
|
41
|
+
<p>${message}</p>
|
|
42
|
+
<p><a href="/">Back to the shop →</a></p>
|
|
43
|
+
</div>
|
|
44
|
+
</body>
|
|
45
|
+
</html>`;
|
|
46
|
+
return new Response(html, {
|
|
47
|
+
status,
|
|
48
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
53
|
+
// Off for marketing tenants — they capture leads via external CTAs.
|
|
54
|
+
if (!context.locals.features.newsletter) {
|
|
55
|
+
return new Response(null, { status: 404 });
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const form = await context.request.formData();
|
|
59
|
+
const email = String(form.get("email") ?? "").trim().toLowerCase();
|
|
60
|
+
const source = String(form.get("source") ?? "unknown").trim();
|
|
61
|
+
|
|
62
|
+
if (!EMAIL_RE.test(email)) {
|
|
63
|
+
return thankYou(
|
|
64
|
+
"That address didn't look quite right — head back and try again.",
|
|
65
|
+
200,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Persist. Phase 1: KV stub. Phase 2: replace with an ESP API call here.
|
|
70
|
+
const kv = await readKv("TENANT_CACHE");
|
|
71
|
+
if (kv) {
|
|
72
|
+
await kv.put(
|
|
73
|
+
`newsletter:${email}`,
|
|
74
|
+
JSON.stringify({ email, source, at: new Date().toISOString() }),
|
|
75
|
+
);
|
|
76
|
+
} else {
|
|
77
|
+
// No KV binding (local/dev): log and move on.
|
|
78
|
+
console.log(`[newsletter] signup ${email} (source: ${source})`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Redirect back where they came from (or home) with a success flag, so the
|
|
82
|
+
// page can show its own confirmation in-context. Falls back to the
|
|
83
|
+
// thank-you page if there's no usable referer.
|
|
84
|
+
const referer = context.request.headers.get("referer");
|
|
85
|
+
if (referer) {
|
|
86
|
+
try {
|
|
87
|
+
const back = new URL(referer);
|
|
88
|
+
back.searchParams.set("subscribed", "1");
|
|
89
|
+
return Response.redirect(back.toString(), 303);
|
|
90
|
+
} catch {
|
|
91
|
+
// fall through to thank-you
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return thankYou("We'll send word when something worth knowing lands.");
|
|
95
|
+
} catch {
|
|
96
|
+
// Never 500 on a signup.
|
|
97
|
+
return thankYou("We'll send word when something worth knowing lands.");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
102
|
+
// Nothing to render here directly — bounce back to the shop.
|
|
103
|
+
return context.redirect("/", 303);
|
|
104
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/request-access — early-access lead capture for the `home` landing page.
|
|
3
|
+
*
|
|
4
|
+
* The browser form (tenants/home) POSTs here same-origin (CSP `connect-src 'self'`
|
|
5
|
+
* + `form-action 'self'`). This route forwards the lead to tot20's generic
|
|
6
|
+
* messaging endpoint SERVER-SIDE — the storefront's ToT credentials never reach
|
|
7
|
+
* the browser. The message emails the Trello Success board so a card is created.
|
|
8
|
+
*
|
|
9
|
+
* tot20 auth wire: like `@/lib/auth/totAccessClient`, tot20's swagger reads
|
|
10
|
+
* machine creds from the request BODY fields `totApiKey`/`totSecretKey` (NOT
|
|
11
|
+
* headers). So we merge those into the JSON POST body.
|
|
12
|
+
*
|
|
13
|
+
* GRACEFUL FALLBACK: the tot20 `/api/messages` endpoint is being built in a
|
|
14
|
+
* parallel workstream (Claim A) and isn't live yet. Until it is — or whenever the
|
|
15
|
+
* send fails / creds are unset — we still return `{ ok: true }` with a
|
|
16
|
+
* `fallback: "mailto"` + a prefilled `mailto:` so the client opens the user's mail
|
|
17
|
+
* client to the board. The form is never a dead end. Once A lands + creds are set,
|
|
18
|
+
* the server-side send just works and no mailto is returned.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately NOT gated on `context.locals.features.*` — unlike newsletter.ts,
|
|
21
|
+
* this must work on the `home` marketing tenant.
|
|
22
|
+
*/
|
|
23
|
+
import type { APIContext } from "astro";
|
|
24
|
+
import { readEnv } from "@/lib/env";
|
|
25
|
+
|
|
26
|
+
export const prerender = false;
|
|
27
|
+
|
|
28
|
+
// Trello Success board — a card is created from the forwarded email.
|
|
29
|
+
// Kept in sync with the mailto: fallback in tenants/home/public/js/storefront.js.
|
|
30
|
+
const BOARD_EMAIL = "darrin84+l6gajmvvoo1x4edew5qs@boards.trello.com";
|
|
31
|
+
|
|
32
|
+
// Permissive email shape — tot20 / the mail system is the real deliverability authority.
|
|
33
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
34
|
+
|
|
35
|
+
interface RequestAccessFields {
|
|
36
|
+
email: string;
|
|
37
|
+
firstName: string;
|
|
38
|
+
lastName: string;
|
|
39
|
+
phone: string;
|
|
40
|
+
details: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function json(body: unknown, status = 200): Response {
|
|
44
|
+
return new Response(JSON.stringify(body), {
|
|
45
|
+
status,
|
|
46
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Read fields from either a JSON body (the fetch form) or urlencoded/multipart (no-JS). */
|
|
51
|
+
async function readFields(request: Request): Promise<RequestAccessFields> {
|
|
52
|
+
const ct = request.headers.get("content-type") || "";
|
|
53
|
+
const pick = (v: unknown) => String(v ?? "").trim();
|
|
54
|
+
if (ct.includes("application/json")) {
|
|
55
|
+
const b = (await request.json().catch(() => ({}))) as Record<string, unknown>;
|
|
56
|
+
return {
|
|
57
|
+
email: pick(b.email),
|
|
58
|
+
firstName: pick(b.firstName),
|
|
59
|
+
lastName: pick(b.lastName),
|
|
60
|
+
phone: pick(b.phone),
|
|
61
|
+
details: pick(b.details),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const form = await request.formData();
|
|
65
|
+
return {
|
|
66
|
+
email: pick(form.get("email")),
|
|
67
|
+
firstName: pick(form.get("firstName")),
|
|
68
|
+
lastName: pick(form.get("lastName")),
|
|
69
|
+
phone: pick(form.get("phone")),
|
|
70
|
+
details: pick(form.get("details")),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Escape user text before it lands in the HTML email body. */
|
|
75
|
+
function esc(s: string): string {
|
|
76
|
+
return s
|
|
77
|
+
.replace(/&/g, "&")
|
|
78
|
+
.replace(/</g, "<")
|
|
79
|
+
.replace(/>/g, ">")
|
|
80
|
+
.replace(/"/g, """);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildHtml(f: RequestAccessFields): string {
|
|
84
|
+
const rows: Array<[string, string]> = [
|
|
85
|
+
["Name", `${f.firstName} ${f.lastName}`],
|
|
86
|
+
["Email", f.email],
|
|
87
|
+
];
|
|
88
|
+
if (f.phone) rows.push(["Phone", f.phone]);
|
|
89
|
+
if (f.details) rows.push(["What they sell", f.details]);
|
|
90
|
+
const body = rows
|
|
91
|
+
.map(([k, v]) => `<p style="margin:0 0 8px"><strong>${esc(k)}:</strong> ${esc(v)}</p>`)
|
|
92
|
+
.join("\n");
|
|
93
|
+
return `<div style="font-family:system-ui,sans-serif;line-height:1.5">
|
|
94
|
+
<h2 style="margin:0 0 12px">Storefront early-access request</h2>
|
|
95
|
+
${body}
|
|
96
|
+
<hr style="margin:16px 0;border:none;border-top:1px solid #ddd">
|
|
97
|
+
<p style="color:#888;font-size:12px;margin:0">Sent from the Storefront landing page (tokenoftrust.com).</p>
|
|
98
|
+
</div>`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Prefilled mailto: to the board — the fallback so the lead is never dropped. */
|
|
102
|
+
function mailtoFallback(f: RequestAccessFields): string {
|
|
103
|
+
const lines = [
|
|
104
|
+
`First name: ${f.firstName}`,
|
|
105
|
+
`Last name: ${f.lastName}`,
|
|
106
|
+
`Email: ${f.email}`,
|
|
107
|
+
f.phone ? `Phone: ${f.phone}` : null,
|
|
108
|
+
f.details ? `\nDetails:\n${f.details}` : null,
|
|
109
|
+
]
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
.join("\n");
|
|
112
|
+
const subject = `Early-access request — ${f.firstName} ${f.lastName}`;
|
|
113
|
+
return `mailto:${BOARD_EMAIL}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(lines)}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
117
|
+
let f: RequestAccessFields;
|
|
118
|
+
try {
|
|
119
|
+
f = await readFields(context.request);
|
|
120
|
+
} catch {
|
|
121
|
+
return json({ ok: false, error: "invalid_body" }, 400);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Validate: email + first + last required.
|
|
125
|
+
if (!EMAIL_RE.test(f.email) || !f.firstName || !f.lastName) {
|
|
126
|
+
return json({ ok: false, error: "missing_required_fields" }, 400);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const subject = `Early-access request — ${f.firstName} ${f.lastName}`;
|
|
130
|
+
|
|
131
|
+
// Try the server-side send via tot20. Any shortfall (unconfigured creds,
|
|
132
|
+
// endpoint not live yet, non-2xx, network error) degrades to the mailto fallback.
|
|
133
|
+
const baseUrl = (await readEnv("TOT_CORE_URL"))?.replace(/\/+$/, "");
|
|
134
|
+
const apiKey = await readEnv("TOT_API_KEY");
|
|
135
|
+
const secretKey = await readEnv("TOT_SECRET_KEY");
|
|
136
|
+
|
|
137
|
+
if (baseUrl && apiKey) {
|
|
138
|
+
try {
|
|
139
|
+
const res = await fetch(`${baseUrl}/api/messages`, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
142
|
+
body: JSON.stringify({
|
|
143
|
+
totApiKey: apiKey,
|
|
144
|
+
...(secretKey ? { totSecretKey: secretKey } : {}),
|
|
145
|
+
channel: "email",
|
|
146
|
+
to: BOARD_EMAIL,
|
|
147
|
+
subject,
|
|
148
|
+
replyTo: f.email,
|
|
149
|
+
html: buildHtml(f),
|
|
150
|
+
}),
|
|
151
|
+
});
|
|
152
|
+
if (res.ok) {
|
|
153
|
+
return json({ ok: true });
|
|
154
|
+
}
|
|
155
|
+
console.warn(`[request-access] tot20 /api/messages returned ${res.status}; using mailto fallback`);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.warn("[request-access] tot20 /api/messages send failed; using mailto fallback", {
|
|
158
|
+
error: err instanceof Error ? err.message : String(err),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
console.log("[request-access] TOT_CORE_URL/TOT_API_KEY unset; using mailto fallback");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Fallback: acknowledge success and hand the client a prefilled mailto so the
|
|
166
|
+
// request still reaches the board via the user's own mail client.
|
|
167
|
+
return json({ ok: true, fallback: "mailto", mailto: mailtoFallback(f) });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
171
|
+
return context.redirect("/", 303);
|
|
172
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* /auth/login — "Sign in with Token of Trust" for a gated preview host.
|
|
4
|
+
*
|
|
5
|
+
* Two-step, no-JS-friendly, all on the tenant's own branded host:
|
|
6
|
+
* step 1: enter your email → POST /api/auth/send
|
|
7
|
+
* step 2: enter the 6-digit code you were emailed → POST /api/auth/verify
|
|
8
|
+
*
|
|
9
|
+
* The page renders instructions + forms only; every auth decision is server-side
|
|
10
|
+
* (tot20 sends/verifies the code, the storefront mints the session). No secrets
|
|
11
|
+
* reach the browser. Inline style carries the per-request CSP nonce.
|
|
12
|
+
*/
|
|
13
|
+
export const prerender = false;
|
|
14
|
+
|
|
15
|
+
const cspNonce = Astro.locals.cspNonce;
|
|
16
|
+
const url = new URL(Astro.request.url);
|
|
17
|
+
const sent = url.searchParams.get("sent") === "1";
|
|
18
|
+
const email = (url.searchParams.get("email") ?? "").trim();
|
|
19
|
+
const errorCode = url.searchParams.get("error");
|
|
20
|
+
const next = (() => {
|
|
21
|
+
const n = url.searchParams.get("next") ?? "/";
|
|
22
|
+
return n.startsWith("/") && !n.startsWith("//") ? n : "/";
|
|
23
|
+
})();
|
|
24
|
+
|
|
25
|
+
const ERROR_MESSAGES: Record<string, string> = {
|
|
26
|
+
bad_email: "That email doesn't look right — please try again.",
|
|
27
|
+
unconfigured: "Sign-in isn't available right now. Please try again shortly.",
|
|
28
|
+
invalid: "That code didn't match. Check it and try again.",
|
|
29
|
+
expired: "That code has expired — request a fresh one.",
|
|
30
|
+
too_many_attempts: "Too many attempts. Request a new code to continue.",
|
|
31
|
+
verify_failed: "We couldn't verify your sign-in. Please request a fresh code and try again.",
|
|
32
|
+
};
|
|
33
|
+
const errorMessage = errorCode ? (ERROR_MESSAGES[errorCode] ?? "Something went wrong — try again.") : null;
|
|
34
|
+
// A stale/failed code should drop back to the email step so a new code can be requested.
|
|
35
|
+
const showCodeStep = sent && errorCode !== "expired" && errorCode !== "too_many_attempts";
|
|
36
|
+
// Local-dev banner (statically dropped from prod). The message depends on the mode:
|
|
37
|
+
// DEV_AUTH=1 → the console bypass is active (no email; code printed to the server console);
|
|
38
|
+
// DEV_AUTH=0 → the REAL path is active (a code IS emailed by the configured tot20/qa, but
|
|
39
|
+
// only to an address that already holds a role). Read the toggle so the banner never lies.
|
|
40
|
+
const devMode = import.meta.env.DEV;
|
|
41
|
+
let devAuthOn = false;
|
|
42
|
+
if (devMode) {
|
|
43
|
+
const { readEnv } = await import("@/lib/env");
|
|
44
|
+
devAuthOn = (await readEnv("DEV_AUTH")) === "1";
|
|
45
|
+
}
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
<html lang="en">
|
|
49
|
+
<head>
|
|
50
|
+
<meta charset="utf-8" />
|
|
51
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
52
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
53
|
+
<title>Sign in — Regulated Storefront by Token of Trust©</title>
|
|
54
|
+
<style nonce={cspNonce}>
|
|
55
|
+
:root { color-scheme: light dark; }
|
|
56
|
+
* { box-sizing: border-box; }
|
|
57
|
+
body {
|
|
58
|
+
font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
59
|
+
margin: 0; min-height: 100vh; display: grid; place-items: center;
|
|
60
|
+
padding: 2rem 1.25rem; background: #f5f4f0; color: #1c1c1c;
|
|
61
|
+
}
|
|
62
|
+
@media (prefers-color-scheme: dark) {
|
|
63
|
+
body { background: #14141a; color: #ececf1; }
|
|
64
|
+
}
|
|
65
|
+
.card {
|
|
66
|
+
width: 100%; max-width: 26rem; background: #fff; border: 1px solid #e6e4de;
|
|
67
|
+
border-radius: 14px; padding: 2rem 1.75rem; box-shadow: 0 6px 24px rgba(0,0,0,.06);
|
|
68
|
+
}
|
|
69
|
+
@media (prefers-color-scheme: dark) {
|
|
70
|
+
.card { background: #1e1e28; border-color: #33333f; box-shadow: none; }
|
|
71
|
+
}
|
|
72
|
+
.eyebrow { font: 600 .68rem/1 ui-monospace, monospace; letter-spacing: .12em;
|
|
73
|
+
text-transform: uppercase; color: #8a7f66; margin: 0 0 .75rem; }
|
|
74
|
+
h1 { font-size: 1.4rem; letter-spacing: -.01em; margin: 0 0 .4rem; }
|
|
75
|
+
.sub { color: #6a6a6a; margin: 0 0 1.5rem; font-size: .95rem; }
|
|
76
|
+
@media (prefers-color-scheme: dark) { .sub { color: #a2a2ad; } }
|
|
77
|
+
label { display: block; font-weight: 600; font-size: .82rem; margin: 0 0 .4rem; }
|
|
78
|
+
input[type=email], input[type=text] {
|
|
79
|
+
width: 100%; padding: .7rem .8rem; font-size: 1rem; border-radius: 9px;
|
|
80
|
+
border: 1px solid #cfccc4; background: #fff; color: inherit;
|
|
81
|
+
}
|
|
82
|
+
@media (prefers-color-scheme: dark) {
|
|
83
|
+
input[type=email], input[type=text] { background: #14141a; border-color: #40404d; }
|
|
84
|
+
}
|
|
85
|
+
input[type=text].code { letter-spacing: .4em; font: 600 1.25rem ui-monospace, monospace; text-align: center; }
|
|
86
|
+
button {
|
|
87
|
+
width: 100%; margin-top: 1rem; padding: .75rem 1rem; border: 0; border-radius: 9px;
|
|
88
|
+
background: #1a1a1a; color: #fff; font-weight: 600; font-size: .95rem; cursor: pointer;
|
|
89
|
+
}
|
|
90
|
+
@media (prefers-color-scheme: dark) { button { background: #ececf1; color: #14141a; } }
|
|
91
|
+
button:hover { opacity: .92; }
|
|
92
|
+
.err { background: #fdecec; border: 1px solid #f5c2c2; color: #9a1c1c;
|
|
93
|
+
border-radius: 9px; padding: .6rem .8rem; font-size: .88rem; margin: 0 0 1.15rem; }
|
|
94
|
+
@media (prefers-color-scheme: dark) { .err { background: #3a1f22; border-color: #6b2f34; color: #f6b9b9; } }
|
|
95
|
+
.dev { background: #eef4ff; border: 1px solid #c3d4f5; color: #234; border-radius: 9px;
|
|
96
|
+
padding: .6rem .8rem; font-size: .82rem; margin: 0 0 1.15rem; }
|
|
97
|
+
.dev code { font: 600 .8em ui-monospace, monospace; }
|
|
98
|
+
@media (prefers-color-scheme: dark) { .dev { background: #1b2740; border-color: #2f4468; color: #cfe0ff; } }
|
|
99
|
+
.note { font-size: .82rem; color: #8a8a8a; margin: 1.1rem 0 0; }
|
|
100
|
+
.who { font-weight: 600; }
|
|
101
|
+
a { color: inherit; }
|
|
102
|
+
.foot { margin-top: 1.5rem; font-size: .72rem; color: #9a9a9a; text-align: center; }
|
|
103
|
+
</style>
|
|
104
|
+
</head>
|
|
105
|
+
<body>
|
|
106
|
+
<main class="card">
|
|
107
|
+
<p class="eyebrow">Token of Trust© · Regulated Storefront</p>
|
|
108
|
+
{devMode && (devAuthOn ? (
|
|
109
|
+
<p class="dev"><strong>Local dev — console bypass</strong> (<code>DEV_AUTH=1</code>). No email is sent;
|
|
110
|
+
your sign-in code is printed to the <strong>server console</strong>. Set <code>DEV_AUTH=0</code> to use real emails.</p>
|
|
111
|
+
) : (
|
|
112
|
+
<p class="dev"><strong>Local dev — live email</strong> (<code>DEV_AUTH=0</code>). A code is emailed by the
|
|
113
|
+
configured tot20 (<code>TOT_CORE_URL</code>), but only to an address that already holds a role. Set
|
|
114
|
+
<code>DEV_AUTH=1</code> for offline console codes.</p>
|
|
115
|
+
))}
|
|
116
|
+
{errorMessage && <p class="err">{errorMessage}</p>}
|
|
117
|
+
|
|
118
|
+
{showCodeStep ? (
|
|
119
|
+
<>
|
|
120
|
+
<h1>Check your email</h1>
|
|
121
|
+
<p class="sub">We sent a 6-digit code to <span class="who">{email}</span> if it has access to this preview. Enter it below.</p>
|
|
122
|
+
<form method="post" action="/api/auth/verify" autocomplete="one-time-code">
|
|
123
|
+
<input type="hidden" name="email" value={email} />
|
|
124
|
+
<input type="hidden" name="next" value={next} />
|
|
125
|
+
<label for="code">Access code</label>
|
|
126
|
+
<input class="code" id="code" name="code" type="text" inputmode="numeric"
|
|
127
|
+
pattern="[0-9]*" maxlength="6" autocomplete="one-time-code" required autofocus />
|
|
128
|
+
<button type="submit">Verify & view preview</button>
|
|
129
|
+
</form>
|
|
130
|
+
<p class="note"><a href={`/auth/login${next !== "/" ? `?next=${encodeURIComponent(next)}` : ""}`}>Use a different email</a> · <a href="mailto:darrin84+l6gajmvvoo1x4edew5qs@boards.trello.com?subject=Regulated%20Storefront%20early%20access%20request">Don't have access? Request early access →</a></p>
|
|
131
|
+
</>
|
|
132
|
+
) : (
|
|
133
|
+
<>
|
|
134
|
+
<h1>Sign in</h1>
|
|
135
|
+
<p class="sub"><span class="who">Token of Trust© Regulated Storefront</span> is in early access. Enter your email — if you have access, we'll send a one-time code.</p>
|
|
136
|
+
<form method="post" action="/api/auth/send">
|
|
137
|
+
<input type="hidden" name="next" value={next} />
|
|
138
|
+
<label for="email">Work email</label>
|
|
139
|
+
<input id="email" name="email" type="email" value={email}
|
|
140
|
+
autocomplete="email" placeholder="you@company.com" required autofocus />
|
|
141
|
+
<button type="submit">Email me a code</button>
|
|
142
|
+
</form>
|
|
143
|
+
<p class="note">Access is granted by role — no signup needed. Don't have access yet? <a href="mailto:darrin84+l6gajmvvoo1x4edew5qs@boards.trello.com?subject=Regulated%20Storefront%20early%20access%20request">Request early access →</a></p>
|
|
144
|
+
</>
|
|
145
|
+
)}
|
|
146
|
+
<p class="foot">Token of Trust© Regulated Storefront</p>
|
|
147
|
+
</main>
|
|
148
|
+
</body>
|
|
149
|
+
</html>
|