@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,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broker-assertion MINTER — the storefront half of the "staff proven by a signed
|
|
3
|
+
* assertion, not a plaintext field" contract (#8453, decision
|
|
4
|
+
* `staff-invite-proof-via-broker`).
|
|
5
|
+
*
|
|
6
|
+
* The earlier flow sent the signed-in staff email to tot20 as a PLAINTEXT
|
|
7
|
+
* `actorEmail`/`email` field and asked tot20 to trust it — a spoof: anything that
|
|
8
|
+
* can reach the operator endpoint could name any staff email. This module instead
|
|
9
|
+
* MINTS a short-lived, storefront-signed RS256 JWT (the "broker assertion") that
|
|
10
|
+
* PROVES the storefront asserts this staff actor. It is presented to tot20 in the
|
|
11
|
+
* `X-ToT-Broker-Assertion` header; tot20 verifies the signature (against the
|
|
12
|
+
* storefront's PUBLISHED public key) + replay (`jti`/`exp`) and derives the staff
|
|
13
|
+
* actor from the VERIFIED `sub` — never from an untrusted field.
|
|
14
|
+
*
|
|
15
|
+
* SERVER-SIDE ONLY. The RS256 PRIVATE key (`STOREFRONT_BROKER_PRIVATE_KEY_B64`)
|
|
16
|
+
* lives only in the Worker/server env and is used only here; it MUST NEVER reach
|
|
17
|
+
* the client bundle. Callers are the server-side API routes (they hold the
|
|
18
|
+
* verified `session.email`), which mint per call and hand the compact JWT to the
|
|
19
|
+
* tot20 client to attach as the header. The clients themselves never see the key.
|
|
20
|
+
*
|
|
21
|
+
* Mirrors the jose usage in `./identityToken.ts` (that module VERIFIES tot's
|
|
22
|
+
* ES256 assertion; this one SIGNS the storefront's RS256 assertion — the inverse
|
|
23
|
+
* direction of the same decoupled-authZ pattern).
|
|
24
|
+
*/
|
|
25
|
+
import { SignJWT, importPKCS8, type CryptoKey } from "jose";
|
|
26
|
+
import { readEnv } from "@/lib/env";
|
|
27
|
+
|
|
28
|
+
/** The audience tot20's broker verifier pins. */
|
|
29
|
+
export const BROKER_AUDIENCE = "tot20-broker";
|
|
30
|
+
|
|
31
|
+
/** The only algorithm the storefront broker signs with (tot20 rejects anything else). */
|
|
32
|
+
export const BROKER_ALG = "RS256";
|
|
33
|
+
|
|
34
|
+
/** Default key id (overridable via `STOREFRONT_BROKER_KID`). Names the published key. */
|
|
35
|
+
export const DEFAULT_BROKER_KID = "storefront-qa-1";
|
|
36
|
+
|
|
37
|
+
/** Default issuer (overridable via `STOREFRONT_BROKER_ISSUER`). */
|
|
38
|
+
export const DEFAULT_BROKER_ISSUER = "storefront";
|
|
39
|
+
|
|
40
|
+
/** Assertion lifetime — deliberately short (replay window). */
|
|
41
|
+
export const BROKER_TTL_SECONDS = 300;
|
|
42
|
+
|
|
43
|
+
/** Env keys the minter reads (server-only). */
|
|
44
|
+
export const BROKER_PRIVATE_KEY_ENV = "STOREFRONT_BROKER_PRIVATE_KEY_B64";
|
|
45
|
+
export const BROKER_KID_ENV = "STOREFRONT_BROKER_KID";
|
|
46
|
+
export const BROKER_ISSUER_ENV = "STOREFRONT_BROKER_ISSUER";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Thrown when the minter can't sign — the private key env is absent/malformed, or
|
|
50
|
+
* no subject was supplied. Routes map this to a 503 ("integration not configured")
|
|
51
|
+
* and FAIL CLOSED: with no proof of the staff actor, the staff call must not go out.
|
|
52
|
+
*/
|
|
53
|
+
export class BrokerAssertionError extends Error {}
|
|
54
|
+
|
|
55
|
+
/** Optional test/DI overrides (avoids touching process env in unit tests). */
|
|
56
|
+
export interface MintBrokerAssertionOverrides {
|
|
57
|
+
/** base64 of the PKCS8 PEM private key (defaults to the env value). */
|
|
58
|
+
privateKeyB64?: string;
|
|
59
|
+
kid?: string;
|
|
60
|
+
issuer?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Import the signing key once per distinct PEM (importPKCS8 is not free). Keyed on
|
|
64
|
+
// the base64 string so a key rotation (new env value) transparently re-imports.
|
|
65
|
+
const keyCache = new Map<string, CryptoKey>();
|
|
66
|
+
|
|
67
|
+
async function loadSigningKey(privateKeyB64: string): Promise<CryptoKey> {
|
|
68
|
+
const cached = keyCache.get(privateKeyB64);
|
|
69
|
+
if (cached) return cached;
|
|
70
|
+
let pem: string;
|
|
71
|
+
try {
|
|
72
|
+
pem = Buffer.from(privateKeyB64, "base64").toString("utf8");
|
|
73
|
+
} catch (cause) {
|
|
74
|
+
throw new BrokerAssertionError(
|
|
75
|
+
"broker assertion: STOREFRONT_BROKER_PRIVATE_KEY_B64 is not valid base64 " +
|
|
76
|
+
(cause instanceof Error ? cause.message : String(cause)),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (!/-----BEGIN (RSA )?PRIVATE KEY-----/.test(pem)) {
|
|
80
|
+
throw new BrokerAssertionError(
|
|
81
|
+
"broker assertion: decoded STOREFRONT_BROKER_PRIVATE_KEY_B64 is not a PEM private key",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
let key: CryptoKey;
|
|
85
|
+
try {
|
|
86
|
+
key = (await importPKCS8(pem, BROKER_ALG)) as CryptoKey;
|
|
87
|
+
} catch (cause) {
|
|
88
|
+
throw new BrokerAssertionError(
|
|
89
|
+
"broker assertion: failed to import the RS256 private key " +
|
|
90
|
+
(cause instanceof Error ? cause.message : String(cause)),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
keyCache.set(privateKeyB64, key);
|
|
94
|
+
return key;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Mint a fresh, storefront-signed broker assertion PROVING `subjectEmail` is the
|
|
99
|
+
* staff actor. Returns the compact RS256 JWT to place in `X-ToT-Broker-Assertion`.
|
|
100
|
+
*
|
|
101
|
+
* Claims: `{ iss, sub:<subjectEmail>, aud:'tot20-broker', jti:<uuid>, iat, exp:iat+300 }`.
|
|
102
|
+
* Header: `{ alg:'RS256', kid, typ:'JWT' }`.
|
|
103
|
+
*
|
|
104
|
+
* FAILS CLOSED: throws {@link BrokerAssertionError} when the private key env is
|
|
105
|
+
* absent/malformed or no subject is supplied — the caller must NOT fall back to an
|
|
106
|
+
* unproven call.
|
|
107
|
+
*/
|
|
108
|
+
export async function mintBrokerAssertion(
|
|
109
|
+
subjectEmail: string,
|
|
110
|
+
overrides: MintBrokerAssertionOverrides = {},
|
|
111
|
+
): Promise<string> {
|
|
112
|
+
const sub = (subjectEmail ?? "").trim().toLowerCase();
|
|
113
|
+
if (!sub) {
|
|
114
|
+
throw new BrokerAssertionError("broker assertion: a subject email is required");
|
|
115
|
+
}
|
|
116
|
+
const privateKeyB64 = overrides.privateKeyB64 ?? (await readEnv(BROKER_PRIVATE_KEY_ENV));
|
|
117
|
+
if (!privateKeyB64) {
|
|
118
|
+
throw new BrokerAssertionError(
|
|
119
|
+
`broker assertion: ${BROKER_PRIVATE_KEY_ENV} is not configured (fail closed)`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const kid = overrides.kid ?? (await readEnv(BROKER_KID_ENV)) ?? DEFAULT_BROKER_KID;
|
|
123
|
+
const issuer = overrides.issuer ?? (await readEnv(BROKER_ISSUER_ENV)) ?? DEFAULT_BROKER_ISSUER;
|
|
124
|
+
|
|
125
|
+
const key = await loadSigningKey(privateKeyB64);
|
|
126
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
127
|
+
return new SignJWT({})
|
|
128
|
+
.setProtectedHeader({ alg: BROKER_ALG, kid, typ: "JWT" })
|
|
129
|
+
.setIssuer(issuer)
|
|
130
|
+
.setSubject(sub)
|
|
131
|
+
.setAudience(BROKER_AUDIENCE)
|
|
132
|
+
.setJti(crypto.randomUUID())
|
|
133
|
+
.setIssuedAt(nowSeconds)
|
|
134
|
+
.setExpirationTime(nowSeconds + BROKER_TTL_SECONDS)
|
|
135
|
+
.sign(key);
|
|
136
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local dev auth — the SINGLE, auditable home for the localhost sign-in bypass.
|
|
3
|
+
*
|
|
4
|
+
* On localhost tot20 isn't reachable, so real sign-in/invite emails can't be sent
|
|
5
|
+
* or verified. This module lets a developer complete sign-in locally: `/api/auth/send`
|
|
6
|
+
* prints a one-time code to the server console + stores it here; `/api/auth/verify`
|
|
7
|
+
* consumes it and mints a session WITHOUT tot20/JWKS.
|
|
8
|
+
*
|
|
9
|
+
* SECURITY — this must never exist in a deployed build:
|
|
10
|
+
* 1. EVERY call site MUST wrap use in `if (import.meta.env.DEV)`. That is a STATIC
|
|
11
|
+
* boolean Vite/Astro evaluates at build time, so the whole branch (and this
|
|
12
|
+
* module's imports) are dead-code-eliminated from the production bundle — the
|
|
13
|
+
* same mechanism the middleware dev guards rely on.
|
|
14
|
+
* 2. `devAuthEnabled()` adds a runtime opt-in (`DEV_AUTH=1` in `.dev.vars`) so even
|
|
15
|
+
* a local dev build does nothing unless the developer explicitly turned it on.
|
|
16
|
+
* Keeping the bypass in one file means "does prod contain the bypass?" is a single
|
|
17
|
+
* grep (`devAuth`), and the production path in send/verify is never weakened.
|
|
18
|
+
*/
|
|
19
|
+
import { readEnv } from "@/lib/env";
|
|
20
|
+
import { devTenantResources } from "@/config/devTenants";
|
|
21
|
+
import { createSession } from "./session.js";
|
|
22
|
+
|
|
23
|
+
/** SESSION-KV key prefix for the one-time dev code (distinct from `authsession:`). */
|
|
24
|
+
export const DEV_AUTH_CODE_PREFIX = "devauthcode:";
|
|
25
|
+
/** Dev code lifetime (KV clamps expirationTtl to >= 60). */
|
|
26
|
+
export const DEV_AUTH_CODE_TTL_SECONDS = 600;
|
|
27
|
+
|
|
28
|
+
/** Minimal KV seam (the `SESSION` binding). */
|
|
29
|
+
interface CodeKv {
|
|
30
|
+
get(key: string): Promise<string | null>;
|
|
31
|
+
put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>;
|
|
32
|
+
delete(key: string): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Runtime opt-in. Call ONLY inside `if (import.meta.env.DEV)` so the whole thing is
|
|
37
|
+
* tree-shaken from prod regardless of any env var.
|
|
38
|
+
*/
|
|
39
|
+
export async function devAuthEnabled(): Promise<boolean> {
|
|
40
|
+
return (await readEnv("DEV_AUTH")) === "1";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A 6-digit numeric one-time code (WebCrypto). */
|
|
44
|
+
export function devAuthCode(): string {
|
|
45
|
+
const n = new Uint32Array(1);
|
|
46
|
+
crypto.getRandomValues(n);
|
|
47
|
+
return String((n[0] ?? 0) % 1_000_000).padStart(6, "0");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Store a freshly-minted code for `email`. */
|
|
51
|
+
export async function storeDevCode(kv: CodeKv, email: string, code: string): Promise<void> {
|
|
52
|
+
await kv.put(`${DEV_AUTH_CODE_PREFIX}${email}`, code, {
|
|
53
|
+
expirationTtl: DEV_AUTH_CODE_TTL_SECONDS,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* One-time consume: returns true iff `code` matches the stored code for `email`,
|
|
59
|
+
* deleting it on success so a code can't be replayed.
|
|
60
|
+
*/
|
|
61
|
+
export async function consumeDevCode(kv: CodeKv, email: string, code: string): Promise<boolean> {
|
|
62
|
+
const stored = await kv.get(`${DEV_AUTH_CODE_PREFIX}${email}`);
|
|
63
|
+
if (!stored || stored !== code) return false;
|
|
64
|
+
await kv.delete(`${DEV_AUTH_CODE_PREFIX}${email}`);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Session fields for a local dev sign-in (option (c)): membership on EVERY registered
|
|
70
|
+
* tenant AND a staff scope, so the dashboard picker, every tenant, and the staff
|
|
71
|
+
* (any-tenant) paths are all exercisable locally. `resource` mirrors the real path's
|
|
72
|
+
* verified `aud` (the storefront app identity, `STOREFRONT_APP_DOMAIN`).
|
|
73
|
+
*/
|
|
74
|
+
export async function devSessionFields(
|
|
75
|
+
email: string,
|
|
76
|
+
): Promise<Parameters<typeof createSession>[1]> {
|
|
77
|
+
const resource = (await readEnv("STOREFRONT_APP_DOMAIN")) || "storefront";
|
|
78
|
+
const tenants = devTenantResources().map((resource) => ({
|
|
79
|
+
resource,
|
|
80
|
+
role: "developer",
|
|
81
|
+
}));
|
|
82
|
+
return {
|
|
83
|
+
email: email.toLowerCase(),
|
|
84
|
+
resource,
|
|
85
|
+
roles: [],
|
|
86
|
+
capability: "member",
|
|
87
|
+
tenants,
|
|
88
|
+
staff: ["staff"],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Coming soon" holding page served to UNAUTHENTICATED visitors of a login-gated
|
|
3
|
+
* tenant (every non-self tenant on a gated host — see `loginGate.ts`).
|
|
4
|
+
*
|
|
5
|
+
* Instead of hard-redirecting to /auth/login, we serve a friendly holding page
|
|
6
|
+
* that withholds the tenant's content but offers a Sign-in button. The button
|
|
7
|
+
* carries the same `?next=` destination the gate would have redirected to, so a
|
|
8
|
+
* successful sign-in lands the visitor back where they started.
|
|
9
|
+
*
|
|
10
|
+
* The platform's own home ("self") tenant is exempt and never sees this — it
|
|
11
|
+
* renders publicly so people can browse + sign up.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Escape a string for safe interpolation into an HTML attribute value. */
|
|
15
|
+
function escapeAttr(s: string): string {
|
|
16
|
+
return s
|
|
17
|
+
.replace(/&/g, "&")
|
|
18
|
+
.replace(/"/g, """)
|
|
19
|
+
.replace(/</g, "<")
|
|
20
|
+
.replace(/>/g, ">");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function gatedHoldingHtml(loginHref: string): string {
|
|
24
|
+
const href = escapeAttr(loginHref);
|
|
25
|
+
return `<!doctype html>
|
|
26
|
+
<html lang="en">
|
|
27
|
+
<head>
|
|
28
|
+
<meta charset="utf-8" />
|
|
29
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
30
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
31
|
+
<title>Coming soon</title>
|
|
32
|
+
<style>
|
|
33
|
+
:root { color-scheme: light dark; }
|
|
34
|
+
* { box-sizing: border-box; }
|
|
35
|
+
html, body { height: 100%; margin: 0; }
|
|
36
|
+
body {
|
|
37
|
+
display: flex; align-items: center; justify-content: center;
|
|
38
|
+
min-height: 100%; padding: 2rem;
|
|
39
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
40
|
+
background: radial-gradient(1200px 800px at 50% -10%, #1e293b, #0f172a 60%, #020617);
|
|
41
|
+
color: #e2e8f0;
|
|
42
|
+
}
|
|
43
|
+
main { max-width: 34rem; text-align: center; }
|
|
44
|
+
.badge {
|
|
45
|
+
display: inline-block; letter-spacing: .18em; text-transform: uppercase;
|
|
46
|
+
font-size: .72rem; font-weight: 600; color: #94a3b8;
|
|
47
|
+
border: 1px solid rgba(148,163,184,.35); border-radius: 999px;
|
|
48
|
+
padding: .35rem .8rem; margin-bottom: 1.75rem;
|
|
49
|
+
}
|
|
50
|
+
h1 { font-size: clamp(2rem, 6vw, 3rem); line-height: 1.1; margin: 0 0 1rem; color: #f8fafc; }
|
|
51
|
+
p { font-size: 1.05rem; line-height: 1.6; color: #cbd5e1; margin: 0 auto 2rem; max-width: 28rem; }
|
|
52
|
+
a.btn {
|
|
53
|
+
display: inline-block; text-decoration: none; font-weight: 600; font-size: 1rem;
|
|
54
|
+
color: #04121f; background: #38bdf8; border-radius: 10px; padding: .8rem 1.6rem;
|
|
55
|
+
transition: background .15s ease;
|
|
56
|
+
}
|
|
57
|
+
a.btn:hover { background: #7dd3fc; }
|
|
58
|
+
</style>
|
|
59
|
+
</head>
|
|
60
|
+
<body>
|
|
61
|
+
<main>
|
|
62
|
+
<span class="badge">Token of Trust</span>
|
|
63
|
+
<h1>Coming soon</h1>
|
|
64
|
+
<p>This storefront isn't open to the public yet. If you have access, sign in to preview it.</p>
|
|
65
|
+
<a class="btn" href="${href}">Sign in</a>
|
|
66
|
+
</main>
|
|
67
|
+
</body>
|
|
68
|
+
</html>`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A `200 OK` holding page — never indexed, never cached. */
|
|
72
|
+
export function gatedHoldingResponse(loginHref: string): Response {
|
|
73
|
+
return new Response(gatedHoldingHtml(loginHref), {
|
|
74
|
+
status: 200,
|
|
75
|
+
headers: {
|
|
76
|
+
"content-type": "text/html; charset=utf-8",
|
|
77
|
+
"cache-control": "no-store",
|
|
78
|
+
"x-robots-tag": "noindex, nofollow",
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline verification of tot's identity assertion (`id_token`) — the consuming
|
|
3
|
+
* half of the decoupled-authZ architecture (Phase 3).
|
|
4
|
+
*
|
|
5
|
+
* tot signs a short-lived ES256 assertion ONCE at sign-in with its PRIVATE key
|
|
6
|
+
* (`/api/internal/access-code/verify` → `id_token`). This module verifies it
|
|
7
|
+
* OFFLINE using tot's published JWKS (fetched once, cached, key-selected by
|
|
8
|
+
* `kid`) and derives the storefront session from the VERIFIED claims — no shared
|
|
9
|
+
* secret, no per-request callback to tot. Removing tot's network reachability
|
|
10
|
+
* after sign-in must not break authorization; the token's own `exp` bounds
|
|
11
|
+
* staleness.
|
|
12
|
+
*
|
|
13
|
+
* This mirrors the Phase-2 launchpad kernel's lightweight offline verifier
|
|
14
|
+
* (`launchpad src/modules/authz/jwks-verifier.ts` + `identity-claims.ts`) and the
|
|
15
|
+
* tot-internal verify path proven in
|
|
16
|
+
* `tot20 services/InternalAuthService/identityTokenService.jest.js`.
|
|
17
|
+
*
|
|
18
|
+
* PROTOTYPE SCOPE: this replicates ONLY the JWKS offline-verify + a coarse
|
|
19
|
+
* claims→session derivation. The full Cedar PDP (`can()` policy engine, staff
|
|
20
|
+
* sandbox) and consuming the launchpad kernel as an installed package are
|
|
21
|
+
* Phase 4 — see branch `feat/authz-cedar-kernel`. Do NOT vendor the Cedar-wasm
|
|
22
|
+
* PDP into the Worker; it is unnecessary for the current coarse gate.
|
|
23
|
+
*
|
|
24
|
+
* Contract: /tmp/authz-token-claims-contract.md (AS-BUILT). Key points mirrored
|
|
25
|
+
* here: §2 (ES256 + jose), §3 (claim set), §5 (consumer verify algorithm),
|
|
26
|
+
* §6 (field name `id_token`), §6b (JWKS host is per-env app config; jose
|
|
27
|
+
* cross-version 4↔6 is wire-compatible by construction).
|
|
28
|
+
*/
|
|
29
|
+
import {
|
|
30
|
+
jwtVerify,
|
|
31
|
+
createRemoteJWKSet,
|
|
32
|
+
createLocalJWKSet,
|
|
33
|
+
decodeProtectedHeader,
|
|
34
|
+
type JWTVerifyGetKey,
|
|
35
|
+
type JSONWebKeySet,
|
|
36
|
+
} from "jose";
|
|
37
|
+
import { readEnv } from "@/lib/env";
|
|
38
|
+
|
|
39
|
+
/** tot's issuer — consumers pin/allowlist this (`auth.jwt.issuer`). */
|
|
40
|
+
export const TOT_ISSUER = "tokenoftrust-internal-ops";
|
|
41
|
+
|
|
42
|
+
/** The only signing algorithm tot uses. Consumers MUST reject anything else. */
|
|
43
|
+
export const TOT_ALG = "ES256";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* JWKS path (host is per-env app config — §6b). Contract also serves
|
|
47
|
+
* `/api/internal/auth/jwks`; the well-known path is the canonical one.
|
|
48
|
+
*/
|
|
49
|
+
export const JWKS_PATH = "/api/.well-known/jwks.json";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Coarse tenant role, collapsed by tot from its per-tenant capability
|
|
53
|
+
* (`developer`/`inviter`/none → `member`). Org standing only; finer granularity
|
|
54
|
+
* is the storefront's own job to derive locally (Phase-4 PDP work).
|
|
55
|
+
*/
|
|
56
|
+
export type CoarseRole = "owner" | "admin" | "member";
|
|
57
|
+
|
|
58
|
+
/** A single tenant membership carried in the assertion. */
|
|
59
|
+
export interface TenantMembership {
|
|
60
|
+
resource: string;
|
|
61
|
+
role: CoarseRole;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The verified identity assertion payload (tot-authoritative + registered
|
|
66
|
+
* claims). Everything here is an INPUT to the storefront's policy, not a
|
|
67
|
+
* decision. Mirrors the launchpad kernel's `IdentityClaims`.
|
|
68
|
+
*/
|
|
69
|
+
export interface IdentityClaims {
|
|
70
|
+
// Registered claims
|
|
71
|
+
iss: string;
|
|
72
|
+
sub: string;
|
|
73
|
+
aud: string;
|
|
74
|
+
iat: number;
|
|
75
|
+
exp: number;
|
|
76
|
+
jti?: string;
|
|
77
|
+
// tot-authoritative claims
|
|
78
|
+
email: string;
|
|
79
|
+
/** Baseline verification signal today (email control proven at sign-in). */
|
|
80
|
+
email_verified: boolean;
|
|
81
|
+
tenants: TenantMembership[];
|
|
82
|
+
/**
|
|
83
|
+
* ToT-staff cross-domain scope (present for ToT staff). CARRIED as a principal
|
|
84
|
+
* attribute; the coarse gate grants NOTHING from it — staff must never elevate
|
|
85
|
+
* app privileges (enforced by the Phase-4 PDP staff sandbox).
|
|
86
|
+
*/
|
|
87
|
+
staff: string[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Coarse-role collapse guard: anything tot didn't send as owner/admin → member. */
|
|
91
|
+
function coerceRole(role: unknown): CoarseRole {
|
|
92
|
+
return role === "owner" || role === "admin" ? role : "member";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Normalize a jose JWT payload into typed {@link IdentityClaims}, coercing the
|
|
97
|
+
* loosely-typed pieces and filling contract defaults (`staff: []`). Throws if a
|
|
98
|
+
* required tot-authoritative claim is missing or malformed. Mirrors the kernel's
|
|
99
|
+
* `toIdentityClaims`.
|
|
100
|
+
*/
|
|
101
|
+
export function toIdentityClaims(payload: Record<string, unknown>): IdentityClaims {
|
|
102
|
+
const email = payload.email;
|
|
103
|
+
if (typeof email !== "string" || !email) {
|
|
104
|
+
throw new Error("identity assertion missing `email` claim");
|
|
105
|
+
}
|
|
106
|
+
const tenantsRaw = Array.isArray(payload.tenants) ? payload.tenants : [];
|
|
107
|
+
const tenants: TenantMembership[] = tenantsRaw.map((t): TenantMembership => {
|
|
108
|
+
const entry = t as { resource?: unknown; role?: unknown };
|
|
109
|
+
if (typeof entry.resource !== "string") {
|
|
110
|
+
throw new Error("identity assertion `tenants[].resource` must be a string");
|
|
111
|
+
}
|
|
112
|
+
return { resource: entry.resource, role: coerceRole(entry.role) };
|
|
113
|
+
});
|
|
114
|
+
const staff = Array.isArray(payload.staff)
|
|
115
|
+
? payload.staff.filter((s): s is string => typeof s === "string")
|
|
116
|
+
: [];
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
iss: String(payload.iss ?? ""),
|
|
120
|
+
sub: String(payload.sub ?? email),
|
|
121
|
+
aud: String(payload.aud ?? ""),
|
|
122
|
+
iat: Number(payload.iat ?? 0),
|
|
123
|
+
exp: Number(payload.exp ?? 0),
|
|
124
|
+
jti: typeof payload.jti === "string" ? payload.jti : undefined,
|
|
125
|
+
email: email.toLowerCase(),
|
|
126
|
+
email_verified: payload.email_verified === true,
|
|
127
|
+
tenants,
|
|
128
|
+
staff,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Derive tot's JWKS URL from the configured tot API base (§6b: path fixed, host app-config). */
|
|
133
|
+
export function deriveJwksUri(baseUrl: string): string {
|
|
134
|
+
return `${baseUrl.replace(/\/+$/, "")}${JWKS_PATH}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface IdentityVerifierConfig {
|
|
138
|
+
/**
|
|
139
|
+
* This host's tenant appDomain / `resource` — enforced as the `aud` claim (tot
|
|
140
|
+
* sets `aud` per-assertion to the target app). REQUIRED: a token minted for
|
|
141
|
+
* another tenant must be rejected.
|
|
142
|
+
*/
|
|
143
|
+
audience: string;
|
|
144
|
+
/** Allowlisted issuer. Defaults to tot's. */
|
|
145
|
+
issuer?: string;
|
|
146
|
+
/**
|
|
147
|
+
* tot's JWKS endpoint, e.g. `https://qa.tokenoftrust.com/api/.well-known/jwks.json`.
|
|
148
|
+
* Provide in production. jose caches the fetch, selects the key by `kid`, and
|
|
149
|
+
* re-fetches once on an unknown `kid` before failing closed.
|
|
150
|
+
*/
|
|
151
|
+
jwksUri?: string;
|
|
152
|
+
/**
|
|
153
|
+
* Pre-resolved JWKS (test / air-gapped path). The storefront holds ONLY the
|
|
154
|
+
* published public material — no private key, no secret. Exactly one of
|
|
155
|
+
* {@link jwksUri} or {@link jwks} must be supplied.
|
|
156
|
+
*/
|
|
157
|
+
jwks?: JSONWebKeySet;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Verifies tot identity assertions offline against the published JWKS. One
|
|
162
|
+
* instance per (audience, JWKS host); its `audience` is fixed. Never calls tot
|
|
163
|
+
* per request — the JWKS fetch is cached inside jose's key resolver, so once the
|
|
164
|
+
* key material is in-process, verification survives tot being unreachable.
|
|
165
|
+
*/
|
|
166
|
+
export class IdentityAssertionVerifier {
|
|
167
|
+
private readonly audience: string;
|
|
168
|
+
private readonly issuer: string;
|
|
169
|
+
private readonly keys: JWTVerifyGetKey;
|
|
170
|
+
|
|
171
|
+
constructor(config: IdentityVerifierConfig) {
|
|
172
|
+
if (!config.audience) {
|
|
173
|
+
throw new Error("IdentityAssertionVerifier requires an `audience` (this host's resource)");
|
|
174
|
+
}
|
|
175
|
+
if (!!config.jwksUri === !!config.jwks) {
|
|
176
|
+
throw new Error("IdentityAssertionVerifier requires exactly one of `jwksUri` or `jwks`");
|
|
177
|
+
}
|
|
178
|
+
this.audience = config.audience;
|
|
179
|
+
this.issuer = config.issuer ?? TOT_ISSUER;
|
|
180
|
+
this.keys = config.jwks
|
|
181
|
+
? createLocalJWKSet(config.jwks)
|
|
182
|
+
: createRemoteJWKSet(new URL(config.jwksUri as string));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Verify a compact JWS and return typed claims. Rejects (throws) on: any
|
|
187
|
+
* `alg` ≠ ES256 (blocks `alg:none` and every HS*), bad signature, wrong/absent
|
|
188
|
+
* `aud`, unlisted `iss`, or expired/not-yet-valid `exp`/`iat`.
|
|
189
|
+
*/
|
|
190
|
+
async verify(token: string): Promise<IdentityClaims> {
|
|
191
|
+
// Fail fast + loud on the algorithm before touching keys. jose's `algorithms`
|
|
192
|
+
// allowlist below is the authoritative guard; this makes the reason explicit.
|
|
193
|
+
const header = decodeProtectedHeader(token);
|
|
194
|
+
if (header.alg !== TOT_ALG) {
|
|
195
|
+
throw new Error(`unexpected JWT alg ${String(header.alg)}; only ${TOT_ALG} is accepted`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const { payload } = await jwtVerify(token, this.keys, {
|
|
199
|
+
issuer: this.issuer,
|
|
200
|
+
audience: this.audience,
|
|
201
|
+
algorithms: [TOT_ALG],
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return toIdentityClaims(payload as Record<string, unknown>);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// --- Verifier cache -----------------------------------------------------------
|
|
209
|
+
//
|
|
210
|
+
// In-memory, module-scoped cache keyed by (jwksUri | audience). Good enough for
|
|
211
|
+
// the prototype: each Worker isolate builds its verifier once and reuses jose's
|
|
212
|
+
// cached JWKS across requests. FOLLOW-UP (multi-instance): promote the JWKS cache
|
|
213
|
+
// to KV or a Durable Object so isolates share fetched key material and honour a
|
|
214
|
+
// single rotation signal. Tracked with the Phase-4 kernel adoption.
|
|
215
|
+
const verifierCache = new Map<string, IdentityAssertionVerifier>();
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get (or build + cache) the verifier for a resource, deriving the JWKS URL from
|
|
219
|
+
* the configured tot API base (`TOT_CORE_URL` — the same base `totAccessClient`
|
|
220
|
+
* posts the access-code endpoints to). Returns `null` when tot's base URL is not
|
|
221
|
+
* configured (routes fail closed). Pass `overrides.jwks`/`overrides.jwksUri` in
|
|
222
|
+
* tests to inject key material without a network fetch.
|
|
223
|
+
*/
|
|
224
|
+
export async function getIdentityVerifier(
|
|
225
|
+
resource: string,
|
|
226
|
+
overrides: { jwksUri?: string; jwks?: JSONWebKeySet; baseUrl?: string } = {},
|
|
227
|
+
): Promise<IdentityAssertionVerifier | null> {
|
|
228
|
+
if (overrides.jwks) {
|
|
229
|
+
// Test / air-gapped: never cached (keys are per-test) and never networked.
|
|
230
|
+
return new IdentityAssertionVerifier({ audience: resource, jwks: overrides.jwks });
|
|
231
|
+
}
|
|
232
|
+
const jwksUri =
|
|
233
|
+
overrides.jwksUri ??
|
|
234
|
+
deriveJwksUri(overrides.baseUrl ?? (await readEnv("TOT_CORE_URL")) ?? "");
|
|
235
|
+
if (jwksUri === JWKS_PATH) return null; // empty base → unconfigured
|
|
236
|
+
|
|
237
|
+
const cacheKey = `${jwksUri}|${resource}`;
|
|
238
|
+
let v = verifierCache.get(cacheKey);
|
|
239
|
+
if (!v) {
|
|
240
|
+
v = new IdentityAssertionVerifier({ audience: resource, jwksUri });
|
|
241
|
+
verifierCache.set(cacheKey, v);
|
|
242
|
+
}
|
|
243
|
+
return v;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get (or build + cache) the FEDERATED verifier — the one for multi-tenant
|
|
248
|
+
* sign-in. Its audience is the STOREFRONT'S OWN app identity (`STOREFRONT_APP_DOMAIN`),
|
|
249
|
+
* because tot20's federated `id_token` sets `aud` = the calling app (storefront),
|
|
250
|
+
* NOT a tenant. The token then carries ALL of the email's memberships in
|
|
251
|
+
* `tenants[]`. Returns null when the tot base URL (`TOT_CORE_URL`) or the
|
|
252
|
+
* storefront app identity (`STOREFRONT_APP_DOMAIN`) is unconfigured — the route
|
|
253
|
+
* fails closed. Pass overrides in tests to inject key material + audience.
|
|
254
|
+
*/
|
|
255
|
+
export async function getFederatedVerifier(
|
|
256
|
+
overrides: { jwksUri?: string; jwks?: JSONWebKeySet; baseUrl?: string; audience?: string } = {},
|
|
257
|
+
): Promise<IdentityAssertionVerifier | null> {
|
|
258
|
+
const audience = overrides.audience ?? (await readEnv("STOREFRONT_APP_DOMAIN")) ?? "";
|
|
259
|
+
if (!audience) return null; // storefront's own app identity must be configured
|
|
260
|
+
if (overrides.jwks) {
|
|
261
|
+
return new IdentityAssertionVerifier({ audience, jwks: overrides.jwks });
|
|
262
|
+
}
|
|
263
|
+
const jwksUri =
|
|
264
|
+
overrides.jwksUri ??
|
|
265
|
+
deriveJwksUri(overrides.baseUrl ?? (await readEnv("TOT_CORE_URL")) ?? "");
|
|
266
|
+
if (jwksUri === JWKS_PATH) return null; // empty base → unconfigured
|
|
267
|
+
const cacheKey = `${jwksUri}|${audience}`;
|
|
268
|
+
let v = verifierCache.get(cacheKey);
|
|
269
|
+
if (!v) {
|
|
270
|
+
v = new IdentityAssertionVerifier({ audience, jwksUri });
|
|
271
|
+
verifierCache.set(cacheKey, v);
|
|
272
|
+
}
|
|
273
|
+
return v;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// --- Session derivation from verified claims ---------------------------------
|
|
277
|
+
|
|
278
|
+
/** The session fields derived from the VERIFIED token — the Phase-3 source of truth. */
|
|
279
|
+
export interface DerivedSessionFields {
|
|
280
|
+
email: string;
|
|
281
|
+
resource: string;
|
|
282
|
+
/** Coarse standing on THIS resource, surfaced as the display capability. */
|
|
283
|
+
capability: CoarseRole | "none";
|
|
284
|
+
/** Display roles derived from the coarse claim (not tot's raw app roles). */
|
|
285
|
+
roles: string[];
|
|
286
|
+
/** Full multi-tenant memberships carried in the token (policy input). */
|
|
287
|
+
tenants: TenantMembership[];
|
|
288
|
+
/** ToT-staff scope carried in the token (sandboxed; grants nothing here). */
|
|
289
|
+
staff: string[];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Derive the storefront session from VERIFIED claims for a given resource — the
|
|
294
|
+
* Phase-3 replacement for trusting tot's flat `{roles, capability}`. Picks the
|
|
295
|
+
* membership matching `resource` and collapses its coarse role to the display
|
|
296
|
+
* capability. Throws if the verified token carries no membership for `resource`
|
|
297
|
+
* (a token for another tenant must not open this gate — belt-and-braces on top
|
|
298
|
+
* of the `aud` check).
|
|
299
|
+
*/
|
|
300
|
+
export function sessionFromClaims(claims: IdentityClaims, resource: string): DerivedSessionFields {
|
|
301
|
+
const membership = claims.tenants.find((t) => t.resource === resource);
|
|
302
|
+
if (!membership) {
|
|
303
|
+
throw new Error(`verified assertion carries no membership for resource "${resource}"`);
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
email: claims.email,
|
|
307
|
+
resource,
|
|
308
|
+
capability: membership.role,
|
|
309
|
+
roles: [membership.role],
|
|
310
|
+
tenants: claims.tenants,
|
|
311
|
+
staff: claims.staff,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** The multi-tenant fields derived from a verified FEDERATED assertion. */
|
|
316
|
+
export interface FederatedSessionFields {
|
|
317
|
+
email: string;
|
|
318
|
+
/** The verified `aud` — the storefront app identity this console session is for. */
|
|
319
|
+
resource: string;
|
|
320
|
+
/** EVERY tenant membership the token carries (the dashboard picker's input). */
|
|
321
|
+
tenants: TenantMembership[];
|
|
322
|
+
/** ToT-staff scope carried in the token (sandboxed; grants nothing here). */
|
|
323
|
+
staff: string[];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Derive the storefront CONSOLE session from a verified FEDERATED assertion. Unlike
|
|
328
|
+
* {@link sessionFromClaims}, this does NOT require (or scope to) a single tenant:
|
|
329
|
+
* it binds ALL of `claims.tenants` so a user with N memberships yields an N-vendor
|
|
330
|
+
* session. `resource` is the verified `aud` (the storefront app identity), not a
|
|
331
|
+
* tenant — it exists only to satisfy the session record shape; the dashboard reads
|
|
332
|
+
* `tenants[]`, and the tenant-scoped preview gate never matches it.
|
|
333
|
+
*/
|
|
334
|
+
export function sessionFromFederatedClaims(claims: IdentityClaims): FederatedSessionFields {
|
|
335
|
+
return {
|
|
336
|
+
email: claims.email,
|
|
337
|
+
resource: claims.aud,
|
|
338
|
+
tenants: claims.tenants,
|
|
339
|
+
staff: claims.staff,
|
|
340
|
+
};
|
|
341
|
+
}
|