@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,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/auth/magic-exchange — exchange a magic-link token for a signed-in session.
|
|
3
|
+
*
|
|
4
|
+
* The POST half of the magic-link flow (see /auth/magic for the GET-safe landing). The
|
|
5
|
+
* one-time `signInToken` (minted by tot20 via developerInvites `issueSignInToken:true`,
|
|
6
|
+
* embedded in the storefront's branded email) is exchanged HERE — only on a POST, only
|
|
7
|
+
* from the human clicking "Confirm sign in", never on a GET (inbox link-scanners fire
|
|
8
|
+
* GET and would burn the token).
|
|
9
|
+
*
|
|
10
|
+
* tot20 verifies + BURNS the token (single-use), resolves memberships fresh, and returns
|
|
11
|
+
* a signed federated `id_token` — identical contract to /api/auth/verify. We verify that
|
|
12
|
+
* assertion OFFLINE against tot's JWKS and derive the session from its claims (never trust
|
|
13
|
+
* unsigned JSON). Fail closed on any gap.
|
|
14
|
+
*/
|
|
15
|
+
import type { APIContext } from "astro";
|
|
16
|
+
import {
|
|
17
|
+
getAccessClient,
|
|
18
|
+
getSessionStore,
|
|
19
|
+
setSessionCookie,
|
|
20
|
+
safeNext,
|
|
21
|
+
} from "@/lib/auth/route";
|
|
22
|
+
import {
|
|
23
|
+
getFederatedVerifier,
|
|
24
|
+
sessionFromFederatedClaims,
|
|
25
|
+
} from "@/lib/auth/identityToken";
|
|
26
|
+
import { createSession, DEFAULT_SESSION_TTL_SECONDS } from "@/lib/auth/session";
|
|
27
|
+
|
|
28
|
+
export const prerender = false;
|
|
29
|
+
|
|
30
|
+
/** Back to the landing page carrying an error (no token in the URL on failure). */
|
|
31
|
+
function magicRedirect(
|
|
32
|
+
context: APIContext,
|
|
33
|
+
params: { error?: string; next?: string },
|
|
34
|
+
): Response {
|
|
35
|
+
const q = new URLSearchParams();
|
|
36
|
+
if (params.error) q.set("error", params.error);
|
|
37
|
+
if (params.next && params.next !== "/dashboard") q.set("next", params.next);
|
|
38
|
+
const qs = q.toString();
|
|
39
|
+
return context.redirect(`/auth/magic${qs ? `?${qs}` : ""}`, 303);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
43
|
+
let token = "";
|
|
44
|
+
let next = "/dashboard";
|
|
45
|
+
try {
|
|
46
|
+
const form = await context.request.formData();
|
|
47
|
+
token = String(form.get("token") ?? "").trim();
|
|
48
|
+
next = safeNext(String(form.get("next") ?? "/dashboard"));
|
|
49
|
+
} catch {
|
|
50
|
+
return magicRedirect(context, { error: "invalid", next });
|
|
51
|
+
}
|
|
52
|
+
if (!token) return magicRedirect(context, { error: "missing", next });
|
|
53
|
+
|
|
54
|
+
const client = await getAccessClient();
|
|
55
|
+
const store = await getSessionStore();
|
|
56
|
+
if (!client || !store) {
|
|
57
|
+
return magicRedirect(context, { error: "unconfigured", next });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Consume the token (single-use; burned server-side by tot20).
|
|
61
|
+
const result = await client.exchangeSignInToken(token);
|
|
62
|
+
if (!result.ok) {
|
|
63
|
+
// reason ∈ invalid|expired|error (tot20's "used" normalizes to invalid).
|
|
64
|
+
const error = result.reason === "expired" ? "expired" : "invalid";
|
|
65
|
+
return magicRedirect(context, { error, next });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Prove authorization offline — fail closed with no flat-JSON fallback.
|
|
69
|
+
const idToken = result.id_token;
|
|
70
|
+
if (!idToken) {
|
|
71
|
+
console.error("auth/magic-exchange: no id_token on exchange; failing closed");
|
|
72
|
+
return magicRedirect(context, { error: "verify_failed", next });
|
|
73
|
+
}
|
|
74
|
+
const verifier = await getFederatedVerifier();
|
|
75
|
+
if (!verifier) {
|
|
76
|
+
console.error("auth/magic-exchange: federated verifier unconfigured");
|
|
77
|
+
return magicRedirect(context, { error: "unconfigured", next });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let derived;
|
|
81
|
+
try {
|
|
82
|
+
const claims = await verifier.verify(idToken); // enforces aud === STOREFRONT_APP_DOMAIN
|
|
83
|
+
derived = sessionFromFederatedClaims(claims);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error("auth/magic-exchange: id_token verification failed; failing closed", {
|
|
86
|
+
error: err instanceof Error ? err.message : String(err),
|
|
87
|
+
});
|
|
88
|
+
return magicRedirect(context, { error: "verify_failed", next });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
92
|
+
const { id } = await createSession(
|
|
93
|
+
store,
|
|
94
|
+
{
|
|
95
|
+
email: derived.email,
|
|
96
|
+
resource: derived.resource,
|
|
97
|
+
roles: [],
|
|
98
|
+
capability: "member",
|
|
99
|
+
tenants: derived.tenants,
|
|
100
|
+
staff: derived.staff,
|
|
101
|
+
},
|
|
102
|
+
DEFAULT_SESSION_TTL_SECONDS,
|
|
103
|
+
nowSeconds,
|
|
104
|
+
);
|
|
105
|
+
setSessionCookie(context, id, DEFAULT_SESSION_TTL_SECONDS);
|
|
106
|
+
return context.redirect(next, 303);
|
|
107
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/auth/send — ask tot20 to email a one-time access code (FEDERATED).
|
|
3
|
+
*
|
|
4
|
+
* Federated multi-tenant sign-in: storefront authenticates as its OWN operator
|
|
5
|
+
* identity and asks tot20 to mail a code to the email if it holds a role on ANY
|
|
6
|
+
* tenant — no `resource` is sent (the resource-scoped `/access-code/send` stays
|
|
7
|
+
* for preview-host gating; console sign-in is federated). ENUMERATION-RESISTANT:
|
|
8
|
+
* the response is identical whether or not the email is eligible — we always
|
|
9
|
+
* redirect to the "enter your code" step. Accepts an HTML form post (no JS needed).
|
|
10
|
+
*/
|
|
11
|
+
import type { APIContext } from "astro";
|
|
12
|
+
import { readKv } from "@/lib/env";
|
|
13
|
+
import {
|
|
14
|
+
EMAIL_RE,
|
|
15
|
+
getAccessClient,
|
|
16
|
+
safeNext,
|
|
17
|
+
loginRedirect,
|
|
18
|
+
} from "@/lib/auth/route";
|
|
19
|
+
import {
|
|
20
|
+
devAuthEnabled,
|
|
21
|
+
devAuthCode,
|
|
22
|
+
storeDevCode,
|
|
23
|
+
DEV_AUTH_CODE_TTL_SECONDS,
|
|
24
|
+
} from "@/lib/auth/devAuth";
|
|
25
|
+
|
|
26
|
+
export const prerender = false;
|
|
27
|
+
|
|
28
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
29
|
+
let email = "";
|
|
30
|
+
let next = "/";
|
|
31
|
+
try {
|
|
32
|
+
const form = await context.request.formData();
|
|
33
|
+
email = String(form.get("email") ?? "").trim().toLowerCase();
|
|
34
|
+
next = safeNext(String(form.get("next") ?? "/"));
|
|
35
|
+
} catch {
|
|
36
|
+
return loginRedirect(context, { error: "invalid", next });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Bad address → send them back to step 1 with a gentle error (no code step).
|
|
40
|
+
if (!EMAIL_RE.test(email)) {
|
|
41
|
+
return loginRedirect(context, { error: "bad_email", next });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// LOCAL DEV AUTH (send side). `import.meta.env.DEV` is a STATIC boolean — this
|
|
45
|
+
// whole branch is tree-shaken out of the production build (same mechanism as the
|
|
46
|
+
// dev guards in middleware/index.ts). It NEVER weakens the hosted path.
|
|
47
|
+
//
|
|
48
|
+
// On localhost tot20 isn't reachable, so no email can be sent. When a developer
|
|
49
|
+
// opts in with `DEV_AUTH=1` in `.dev.vars`, we instead mint a one-time code,
|
|
50
|
+
// store it in the SESSION KV, and PRINT it to the server console so the developer
|
|
51
|
+
// can complete sign-in locally. (The verify side that ACCEPTS this code + mints a
|
|
52
|
+
// session without tot20 is the security-sensitive half — see the handoff
|
|
53
|
+
// `2026-07-09-storefront-local-dev-auth-mode.md`; until it lands, the code is
|
|
54
|
+
// stored + printed but sign-in completes only once that verify path is wired.)
|
|
55
|
+
if (import.meta.env.DEV && (await devAuthEnabled())) {
|
|
56
|
+
const kv = await readKv("SESSION");
|
|
57
|
+
if (kv) {
|
|
58
|
+
const code = devAuthCode();
|
|
59
|
+
await storeDevCode(kv, email, code);
|
|
60
|
+
// eslint-disable-next-line no-console -- dev-only server-console code delivery
|
|
61
|
+
console.log(
|
|
62
|
+
`\n 🔑 [DEV AUTH] sign-in code for ${email}: ${code}\n` +
|
|
63
|
+
` valid ${DEV_AUTH_CODE_TTL_SECONDS / 60}m · printed here only (no email in local dev)\n`,
|
|
64
|
+
);
|
|
65
|
+
} else {
|
|
66
|
+
// eslint-disable-next-line no-console -- dev-only diagnostic
|
|
67
|
+
console.warn(
|
|
68
|
+
"[DEV AUTH] DEV_AUTH=1 but no SESSION KV is bound — cannot store a dev code.",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
// Same opaque redirect as the real path (keeps the UX identical).
|
|
72
|
+
return loginRedirect(context, { sent: true, email, next });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const client = await getAccessClient();
|
|
76
|
+
if (!client) {
|
|
77
|
+
// Integration not configured — don't pretend we sent a code.
|
|
78
|
+
return loginRedirect(context, { error: "unconfigured", email, next });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Fire-and-neutral: even a failure here resolves opaque so we never leak
|
|
82
|
+
// whether the address is a role-holder.
|
|
83
|
+
await client.sendFederatedCode(email);
|
|
84
|
+
return loginRedirect(context, { sent: true, email, next });
|
|
85
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/auth/verify — verify the one-time code and mint the FEDERATED,
|
|
3
|
+
* multi-tenant signed-in session.
|
|
4
|
+
*
|
|
5
|
+
* Federated sign-in: storefront calls tot20's `/access-code/federated-verify`
|
|
6
|
+
* (as its own operator identity, no `resource`). tot20 checks the code and returns
|
|
7
|
+
* a signed `id_token` whose `aud` = the STOREFRONT app identity (not a tenant) and
|
|
8
|
+
* whose `tenants[]` carries ALL of the email's memberships.
|
|
9
|
+
*
|
|
10
|
+
* We verify that assertion OFFLINE against tot's JWKS with `aud` pinned to the
|
|
11
|
+
* storefront's configured app identity (`STOREFRONT_APP_DOMAIN`), then bind EVERY
|
|
12
|
+
* membership into the session — so the dashboard picker sees every vendor. Fail
|
|
13
|
+
* closed if the signed assertion is missing or verification fails (never trust the
|
|
14
|
+
* flat JSON). See `@/lib/auth/identityToken`.
|
|
15
|
+
*/
|
|
16
|
+
import type { APIContext } from "astro";
|
|
17
|
+
import { readKv } from "@/lib/env";
|
|
18
|
+
import {
|
|
19
|
+
getAccessClient,
|
|
20
|
+
getSessionStore,
|
|
21
|
+
setSessionCookie,
|
|
22
|
+
safeNext,
|
|
23
|
+
loginRedirect,
|
|
24
|
+
} from "@/lib/auth/route";
|
|
25
|
+
import {
|
|
26
|
+
getFederatedVerifier,
|
|
27
|
+
sessionFromFederatedClaims,
|
|
28
|
+
} from "@/lib/auth/identityToken";
|
|
29
|
+
import { createSession, DEFAULT_SESSION_TTL_SECONDS } from "@/lib/auth/session";
|
|
30
|
+
import { devAuthEnabled, consumeDevCode, devSessionFields } from "@/lib/auth/devAuth";
|
|
31
|
+
|
|
32
|
+
export const prerender = false;
|
|
33
|
+
|
|
34
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
35
|
+
let email = "";
|
|
36
|
+
let code = "";
|
|
37
|
+
let next = "/";
|
|
38
|
+
try {
|
|
39
|
+
const form = await context.request.formData();
|
|
40
|
+
email = String(form.get("email") ?? "").trim().toLowerCase();
|
|
41
|
+
code = String(form.get("code") ?? "").trim();
|
|
42
|
+
next = safeNext(String(form.get("next") ?? "/"));
|
|
43
|
+
} catch {
|
|
44
|
+
return loginRedirect(context, { error: "invalid", next });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!email || !code) {
|
|
48
|
+
return loginRedirect(context, { sent: true, email, error: "invalid", next });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// LOCAL DEV AUTH (verify side). `import.meta.env.DEV` is a STATIC boolean — this
|
|
52
|
+
// whole branch (and the devAuth imports) are tree-shaken out of the production
|
|
53
|
+
// build, so the hosted path below (offline JWKS verification, fail-closed) is
|
|
54
|
+
// never weakened. Second gate: the DEV_AUTH opt-in. Consume the console-printed
|
|
55
|
+
// code stored by /api/auth/send and mint a local session WITHOUT tot20 — the
|
|
56
|
+
// localhost completion of the invite→sign-in loop. See @/lib/auth/devAuth.
|
|
57
|
+
if (import.meta.env.DEV && (await devAuthEnabled())) {
|
|
58
|
+
const store = await getSessionStore();
|
|
59
|
+
const kv = await readKv("SESSION");
|
|
60
|
+
if (!store || !kv) {
|
|
61
|
+
return loginRedirect(context, { sent: true, email, error: "unconfigured", next });
|
|
62
|
+
}
|
|
63
|
+
if (!(await consumeDevCode(kv, email, code))) {
|
|
64
|
+
// Same "code didn't match" UX as the real path (wrong/expired/replayed code).
|
|
65
|
+
return loginRedirect(context, { sent: true, email, error: "invalid", next });
|
|
66
|
+
}
|
|
67
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
68
|
+
const { id } = await createSession(
|
|
69
|
+
store,
|
|
70
|
+
await devSessionFields(email),
|
|
71
|
+
DEFAULT_SESSION_TTL_SECONDS,
|
|
72
|
+
nowSeconds,
|
|
73
|
+
);
|
|
74
|
+
setSessionCookie(context, id, DEFAULT_SESSION_TTL_SECONDS);
|
|
75
|
+
return context.redirect(next, 303);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const client = await getAccessClient();
|
|
79
|
+
const store = await getSessionStore();
|
|
80
|
+
if (!client || !store) {
|
|
81
|
+
return loginRedirect(context, { sent: true, email, error: "unconfigured", next });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result = await client.verifyFederatedCode(email, code);
|
|
85
|
+
if (!result.ok) {
|
|
86
|
+
// Show the code step again with the specific reason (invalid/expired/too_many_attempts).
|
|
87
|
+
return loginRedirect(context, { sent: true, email, error: result.reason, next });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Prove authorization offline. Fail closed (no flat-JSON fallback) if the signed
|
|
91
|
+
// assertion is missing or fails verification.
|
|
92
|
+
const idToken = result.id_token;
|
|
93
|
+
if (!idToken) {
|
|
94
|
+
console.error("auth/verify: no id_token on federated result; failing closed");
|
|
95
|
+
return loginRedirect(context, { sent: true, email, error: "verify_failed", next });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// aud is the STOREFRONT app identity (STOREFRONT_APP_DOMAIN), not a tenant.
|
|
99
|
+
const verifier = await getFederatedVerifier();
|
|
100
|
+
if (!verifier) {
|
|
101
|
+
console.error("auth/verify: federated verifier unconfigured (TOT_CORE_URL/STOREFRONT_APP_DOMAIN)");
|
|
102
|
+
return loginRedirect(context, { sent: true, email, error: "unconfigured", next });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let derived;
|
|
106
|
+
try {
|
|
107
|
+
const claims = await verifier.verify(idToken); // enforces aud === STOREFRONT_APP_DOMAIN
|
|
108
|
+
// Bind EVERY membership — no single-tenant scoping (multi-vendor picker input).
|
|
109
|
+
derived = sessionFromFederatedClaims(claims);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error("auth/verify: federated id_token verification failed; failing closed", {
|
|
112
|
+
error: err instanceof Error ? err.message : String(err),
|
|
113
|
+
});
|
|
114
|
+
return loginRedirect(context, { sent: true, email, error: "verify_failed", next });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
118
|
+
const { id } = await createSession(
|
|
119
|
+
store,
|
|
120
|
+
{
|
|
121
|
+
email: derived.email,
|
|
122
|
+
// The verified aud (storefront app identity) — the console session's scope.
|
|
123
|
+
resource: derived.resource,
|
|
124
|
+
// Per-tenant roles live in tenants[]; the console session itself is role-neutral.
|
|
125
|
+
roles: [],
|
|
126
|
+
capability: "member",
|
|
127
|
+
tenants: derived.tenants,
|
|
128
|
+
staff: derived.staff,
|
|
129
|
+
},
|
|
130
|
+
DEFAULT_SESSION_TTL_SECONDS,
|
|
131
|
+
nowSeconds,
|
|
132
|
+
);
|
|
133
|
+
setSessionCookie(context, id, DEFAULT_SESSION_TTL_SECONDS);
|
|
134
|
+
return context.redirect(next, 303);
|
|
135
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/cache-purge — bump a tenant's edge page-cache version.
|
|
3
|
+
*
|
|
4
|
+
* The ETL load step calls this per tenant after a (re)migration so freshly
|
|
5
|
+
* migrated content is visible immediately, rather than waiting out the page
|
|
6
|
+
* TTL. This closes the loop that HttpToTClient.purgeTenant() (catalog API cache)
|
|
7
|
+
* only half-covered: that purges the data cache; this purges the rendered HTML.
|
|
8
|
+
*
|
|
9
|
+
* Auth: `Authorization: Bearer <CACHE_PURGE_SECRET>`. JSON bodies are exempt from
|
|
10
|
+
* Astro's CSRF origin check, so the server-to-server ETL caller works.
|
|
11
|
+
*/
|
|
12
|
+
import type { APIContext } from "astro";
|
|
13
|
+
import { readEnv, readKv } from "@/lib/env";
|
|
14
|
+
import { bumpCacheVersion } from "@/lib/edgeCache";
|
|
15
|
+
|
|
16
|
+
export const prerender = false;
|
|
17
|
+
|
|
18
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
19
|
+
const secret = await readEnv("CACHE_PURGE_SECRET");
|
|
20
|
+
const auth = context.request.headers.get("authorization");
|
|
21
|
+
if (!secret || auth !== `Bearer ${secret}`) {
|
|
22
|
+
return new Response("Unauthorized", { status: 401 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const kv = await readKv("TENANT_CACHE");
|
|
26
|
+
if (!kv) {
|
|
27
|
+
return new Response("No cache store bound", { status: 503 });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let tenantId: string | undefined;
|
|
31
|
+
try {
|
|
32
|
+
tenantId = ((await context.request.json()) as { tenantId?: string })?.tenantId;
|
|
33
|
+
} catch {
|
|
34
|
+
/* fall through to 400 */
|
|
35
|
+
}
|
|
36
|
+
if (!tenantId) {
|
|
37
|
+
return new Response("tenantId required", { status: 400 });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const version = await bumpCacheVersion(kv, tenantId);
|
|
41
|
+
return new Response(JSON.stringify({ tenantId, version }), {
|
|
42
|
+
headers: { "content-type": "application/json" },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/content-edit/ai-edit — the `/dev` page's "Build with Claude" live demo
|
|
3
|
+
* (WS5 G4). Same shape as `tot dev`'s inline-AI loop (G1): a prompt turns into
|
|
4
|
+
* a reviewable per-file diff via tot-mcp's `content_edit` tool.
|
|
5
|
+
*
|
|
6
|
+
* AUTH (see the G4 design note — pending WS2/beta confirmation): `/dev`'s own
|
|
7
|
+
* signed-in-viewer session (`readViewerSession`) is the developer-level gate,
|
|
8
|
+
* same as the rest of `/dev`; the MCP call itself authenticates as the
|
|
9
|
+
* storefront's own TENANT-OPERATOR credential (`getContentEditClient`, the
|
|
10
|
+
* SAME credential_validate path `/api/changes/accept` already uses) — a
|
|
11
|
+
* server-to-server trust boundary, not the individual developer's identity.
|
|
12
|
+
*
|
|
13
|
+
* CONTENT SOURCE (flagged, not yet real): this demo edits a small, fixed
|
|
14
|
+
* SANDBOX fixture below — NOT the caller's real tenant content. Wiring a real
|
|
15
|
+
* sandbox tenant's live content/theme through the customization/publish
|
|
16
|
+
* pipeline (LookFoundation) is follow-up work; this proves the live loop
|
|
17
|
+
* (prompt → content_edit → diff) end to end without that dependency.
|
|
18
|
+
*/
|
|
19
|
+
import type { APIContext } from "astro";
|
|
20
|
+
import { readViewerSession } from "@/lib/auth/route";
|
|
21
|
+
import { getContentEditClient } from "@/lib/content-edit";
|
|
22
|
+
import { json } from "@/lib/grants/route.js";
|
|
23
|
+
|
|
24
|
+
export const prerender = false;
|
|
25
|
+
|
|
26
|
+
/** The `/dev` demo's fixed sandbox content — see the module header. */
|
|
27
|
+
const SANDBOX_FILES = [
|
|
28
|
+
{
|
|
29
|
+
path: "content/home.html",
|
|
30
|
+
content: "<!doctype html>\n<title>Sandbox Store</title>\n<h1>Welcome to Sandbox Store</h1>\n",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
path: "theme.json",
|
|
34
|
+
content: JSON.stringify({ color: { bg: "#faf9f5", text: "#16191d", accent: "#e07a5f" } }, null, 2),
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
39
|
+
const session = await readViewerSession({ cookies: context.cookies });
|
|
40
|
+
if (!session) return json({ error: "sign in required" }, 401);
|
|
41
|
+
if (!session.capability || session.capability === "none") {
|
|
42
|
+
return json({ error: "no developer capability on this account" }, 403);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let body: { prompt?: unknown };
|
|
46
|
+
try {
|
|
47
|
+
body = (await context.request.json()) as typeof body;
|
|
48
|
+
} catch {
|
|
49
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
50
|
+
}
|
|
51
|
+
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
|
52
|
+
if (!prompt) return json({ error: "prompt is required" }, 400);
|
|
53
|
+
|
|
54
|
+
const client = await getContentEditClient();
|
|
55
|
+
if (client instanceof Response) return client;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const result = await client.editContent(prompt, SANDBOX_FILES);
|
|
59
|
+
return json({ ok: true, ...result });
|
|
60
|
+
} catch (err) {
|
|
61
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
62
|
+
return json({ ok: false, error: message }, 502);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSP violation collector (enables the report-only → enforce
|
|
3
|
+
* rollout). Browsers POST violation reports here (via `report-to` /
|
|
4
|
+
* `report-uri`); we log them so platform + tenant-script CSP violations surface
|
|
5
|
+
* before we flip the policy from Report-Only to enforcing. Returns 204.
|
|
6
|
+
*
|
|
7
|
+
* Phase 1: log to the worker console. A later iteration can forward to an
|
|
8
|
+
* analytics/observability sink.
|
|
9
|
+
*/
|
|
10
|
+
import type { APIRoute } from "astro";
|
|
11
|
+
|
|
12
|
+
export const prerender = false;
|
|
13
|
+
|
|
14
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
15
|
+
try {
|
|
16
|
+
const body = await request.text();
|
|
17
|
+
if (body) {
|
|
18
|
+
// Reports can be `application/csp-report` or `application/reports+json`.
|
|
19
|
+
console.warn("[csp-report]", body.slice(0, 4000));
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
/* never fail a report submission */
|
|
23
|
+
}
|
|
24
|
+
return new Response(null, { status: 204 });
|
|
25
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/dashboard/enter-vendor { vendorAppDomain } — the ToT-STAFF admission
|
|
3
|
+
* endpoint (#8425 staff access). Phase 2 of two-phase admission: on the staff
|
|
4
|
+
* viewer explicitly selecting a searched vendor, tot20 writes the server-side
|
|
5
|
+
* AUDIT row and returns the staffRoles-scoped capability; we then stamp a
|
|
6
|
+
* short-lived {@link StaffSelection} on the session and admit the viewer to
|
|
7
|
+
* `/dashboard/<appDomain>` scoped to that capability.
|
|
8
|
+
*
|
|
9
|
+
* AUTHORIZATION — two independent axes:
|
|
10
|
+
* NEAR (this endpoint): a valid `tot_session` carrying ToT-STAFF scope
|
|
11
|
+
* (`staff[]` non-empty). No session → 401; no staff scope → 403.
|
|
12
|
+
* FAR (tot20): `/clients/staffVendorAccess`, operator-authed as storefront,
|
|
13
|
+
* presenting a storefront-signed staff broker assertion in the
|
|
14
|
+
* `X-ToT-Broker-Assertion` header (minted from the verified session email).
|
|
15
|
+
* tot20 verifies it, re-checks staff scope from its `sub`, scopes the capability,
|
|
16
|
+
* and writes the audit row — it is the authority. No plaintext email on the wire.
|
|
17
|
+
*
|
|
18
|
+
* The staff selection is a DISTINCT access path from `tenants[]`: we NEVER
|
|
19
|
+
* synthesize a membership. The dashboard guard + login gate consult the marker
|
|
20
|
+
* separately (see tenantSelection.staffAdmits / loginGate.sessionAdmitsHost).
|
|
21
|
+
*/
|
|
22
|
+
import type { APIContext } from "astro";
|
|
23
|
+
import { readViewerSession, getSessionMutationHandle } from "@/lib/auth/route";
|
|
24
|
+
import { updateSession } from "@/lib/auth/session";
|
|
25
|
+
import { isStaffSession, buildStaffSelection } from "@/lib/dashboard/staffAdmission";
|
|
26
|
+
import {
|
|
27
|
+
createStaffVendorAccessClient,
|
|
28
|
+
staffAccessHttpStatus,
|
|
29
|
+
StaffVendorAccessClientError,
|
|
30
|
+
} from "@/lib/dashboard/staffVendorAccess";
|
|
31
|
+
import { mintBrokerAssertion, BrokerAssertionError } from "@/lib/auth/brokerAssertion";
|
|
32
|
+
|
|
33
|
+
export const prerender = false;
|
|
34
|
+
|
|
35
|
+
/** JSON response with no-store (staff access state must never be edge-cached). */
|
|
36
|
+
function json(body: unknown, status = 200): Response {
|
|
37
|
+
return new Response(JSON.stringify(body), {
|
|
38
|
+
status,
|
|
39
|
+
headers: { "content-type": "application/json", "cache-control": "no-store" },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
44
|
+
const session = await readViewerSession(context);
|
|
45
|
+
if (!session) return json({ error: "not signed in" }, 401);
|
|
46
|
+
if (!isStaffSession(session)) {
|
|
47
|
+
return json({ error: "staff access required", reason: "notStaff" }, 403);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let body: { vendorAppDomain?: unknown };
|
|
51
|
+
try {
|
|
52
|
+
body = (await context.request.json()) as typeof body;
|
|
53
|
+
} catch {
|
|
54
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
55
|
+
}
|
|
56
|
+
const vendorAppDomain =
|
|
57
|
+
typeof body.vendorAppDomain === "string" ? body.vendorAppDomain.trim() : "";
|
|
58
|
+
if (!vendorAppDomain) return json({ error: "vendorAppDomain is required" }, 400);
|
|
59
|
+
|
|
60
|
+
let client;
|
|
61
|
+
try {
|
|
62
|
+
client = await createStaffVendorAccessClient();
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (err instanceof StaffVendorAccessClientError) {
|
|
65
|
+
return json({ error: "staff-access integration not configured" }, 503);
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// #8453 `staff-invite-proof-via-broker`: prove the staff actor with a fresh
|
|
71
|
+
// storefront-signed assertion minted from the VERIFIED session email. Fail closed
|
|
72
|
+
// (503) if the signing key is unconfigured.
|
|
73
|
+
let brokerAssertion: string;
|
|
74
|
+
try {
|
|
75
|
+
brokerAssertion = await mintBrokerAssertion(session.email);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err instanceof BrokerAssertionError) {
|
|
78
|
+
return json({ error: "staff-proof signing not configured" }, 503);
|
|
79
|
+
}
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// FAR authority: tot20 verifies the assertion, derives the staff actor, scopes the
|
|
84
|
+
// capability, and writes the audit row. We admit ONLY on its ok.
|
|
85
|
+
const result = await client.grantStaffAccess(brokerAssertion, vendorAppDomain);
|
|
86
|
+
if (!result.ok) {
|
|
87
|
+
return json(
|
|
88
|
+
{ error: result.message, reason: result.reason, code: result.code },
|
|
89
|
+
staffAccessHttpStatus(result.reason, result.status),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Stamp the distinct staff-selection marker on the session (short-lived, scoped
|
|
94
|
+
// to the returned capability). Never a tenants[] entry.
|
|
95
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
96
|
+
const selection = buildStaffSelection({
|
|
97
|
+
resource: vendorAppDomain,
|
|
98
|
+
capability: result.capability,
|
|
99
|
+
nowSeconds,
|
|
100
|
+
});
|
|
101
|
+
const handle = await getSessionMutationHandle(context);
|
|
102
|
+
if (!handle) {
|
|
103
|
+
// No SESSION KV / no cookie (e.g. plain `astro dev`) — the audit row is
|
|
104
|
+
// written but we can't persist the marker. Fail closed so the viewer isn't
|
|
105
|
+
// told they're admitted when the guard can't see it.
|
|
106
|
+
return json({ error: "session store unavailable; cannot persist admission" }, 503);
|
|
107
|
+
}
|
|
108
|
+
const updated = await updateSession(
|
|
109
|
+
handle.store,
|
|
110
|
+
handle.id,
|
|
111
|
+
(rec) => ({ ...rec, staffSelection: selection }),
|
|
112
|
+
nowSeconds,
|
|
113
|
+
);
|
|
114
|
+
if (!updated) {
|
|
115
|
+
return json({ error: "session expired; sign in again" }, 401);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return json({
|
|
119
|
+
ok: true,
|
|
120
|
+
appDomain: vendorAppDomain,
|
|
121
|
+
capability: result.capability,
|
|
122
|
+
redirect: `/dashboard/${encodeURIComponent(vendorAppDomain)}`,
|
|
123
|
+
});
|
|
124
|
+
}
|