@tokenoftrust/storefront-runner 1.3.2
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 +42 -0
- package/apps/storefront/astro.config.mjs +66 -0
- package/apps/storefront/dev-plugins/dev-loop-state.mjs +147 -0
- package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +238 -0
- package/apps/storefront/env.d.ts +162 -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 +42 -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/public/shared/commerce-chrome.css +369 -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 +55 -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/admin/AdminAIWorkflowsTab.astro +531 -0
- package/apps/storefront/src/components/admin/AdminCustomersTab.astro +597 -0
- package/apps/storefront/src/components/admin/AdminFoxyCartSettingsPanel.astro +227 -0
- package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +465 -0
- package/apps/storefront/src/components/admin/AdminProductsTab.astro +901 -0
- package/apps/storefront/src/components/admin/AdminReportingTab.astro +435 -0
- package/apps/storefront/src/components/admin/AdminSettingsTab.astro +782 -0
- package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +1223 -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 +201 -0
- package/apps/storefront/src/components/chrome/SiteFooter.astro +201 -0
- package/apps/storefront/src/components/chrome/SiteHeader.astro +234 -0
- package/apps/storefront/src/components/chrome/UtilityNav.astro +79 -0
- package/apps/storefront/src/components/commerce/ArticleCard.astro +242 -0
- package/apps/storefront/src/components/commerce/BrandGrid.astro +175 -0
- package/apps/storefront/src/components/commerce/FoxyLoader.astro +23 -0
- package/apps/storefront/src/components/commerce/FoxySidecartStyles.astro +61 -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/commerce/ReviewList.astro +54 -0
- package/apps/storefront/src/components/commerce/ReviewSummary.astro +44 -0
- package/apps/storefront/src/components/commerce/TagFilterCloud.astro +149 -0
- package/apps/storefront/src/components/compliance/AdultSignatureNotice.astro +37 -0
- package/apps/storefront/src/components/compliance/ComplianceNotice.astro +99 -0
- package/apps/storefront/src/components/compliance/ExciseTaxNotice.astro +45 -0
- package/apps/storefront/src/components/compliance/NicotineWarning.astro +33 -0
- package/apps/storefront/src/components/compliance/PactActNotice.astro +35 -0
- package/apps/storefront/src/components/compliance/Prop65Warning.astro +80 -0
- package/apps/storefront/src/components/compliance/PurchaseLimitNotice.astro +38 -0
- package/apps/storefront/src/components/compliance/ShippingRestrictionNotice.astro +35 -0
- package/apps/storefront/src/components/compliance/StateEligibilityNotice.astro +53 -0
- package/apps/storefront/src/components/home/Hero.astro +164 -0
- package/apps/storefront/src/components/islands/AgeGate.tsx +109 -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 +375 -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/membership/IllustrativeLabel.astro +56 -0
- package/apps/storefront/src/components/membership/TierCards.astro +164 -0
- package/apps/storefront/src/components/membership/TierComparisonTable.astro +148 -0
- package/apps/storefront/src/components/plp/ActiveFilters.astro +79 -0
- package/apps/storefront/src/components/plp/FacetSidebar.astro +199 -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/components/style-guide/StyleGuideNav.astro +95 -0
- package/apps/storefront/src/components/subscription/HowItWorksSteps.astro +67 -0
- package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +67 -0
- package/apps/storefront/src/components/subscription/SavingsExplainer.astro +101 -0
- package/apps/storefront/src/config/adminTargetTenant.ts +28 -0
- package/apps/storefront/src/config/devTenantSeed.ts +78 -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 +102 -0
- package/apps/storefront/src/config/tenants.ts +87 -0
- package/apps/storefront/src/layouts/Layout.astro +368 -0
- package/apps/storefront/src/lib/admin/README.md +122 -0
- package/apps/storefront/src/lib/admin/ai/approvalMode.ts +21 -0
- package/apps/storefront/src/lib/admin/ai/askEnvelope.ts +48 -0
- package/apps/storefront/src/lib/admin/ai/audit.ts +14 -0
- package/apps/storefront/src/lib/admin/ai/index.ts +6 -0
- package/apps/storefront/src/lib/admin/ai/operationalRuns.ts +155 -0
- package/apps/storefront/src/lib/admin/ai/recommendation.ts +20 -0
- package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +172 -0
- package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +65 -0
- package/apps/storefront/src/lib/admin/envelope.ts +10 -0
- package/apps/storefront/src/lib/admin/evidence/evidenceReference.ts +32 -0
- package/apps/storefront/src/lib/admin/memoizeByEnvelope.ts +27 -0
- package/apps/storefront/src/lib/admin/orderClient.ts +146 -0
- package/apps/storefront/src/lib/admin/seed.ts +54 -0
- package/apps/storefront/src/lib/admin/services/mock/aiWorkflows.ts +59 -0
- package/apps/storefront/src/lib/admin/services/mock/customers.ts +86 -0
- package/apps/storefront/src/lib/admin/services/mock/evidence.ts +99 -0
- package/apps/storefront/src/lib/admin/services/mock/foxyCartSettings.ts +78 -0
- package/apps/storefront/src/lib/admin/services/mock/fulfillment.ts +62 -0
- package/apps/storefront/src/lib/admin/services/mock/index.ts +12 -0
- package/apps/storefront/src/lib/admin/services/mock/orders.ts +184 -0
- package/apps/storefront/src/lib/admin/services/mock/products.ts +105 -0
- package/apps/storefront/src/lib/admin/services/mock/reporting.ts +51 -0
- package/apps/storefront/src/lib/admin/services/mock/settings.ts +70 -0
- package/apps/storefront/src/lib/admin/services/mock/tenants.ts +29 -0
- package/apps/storefront/src/lib/admin/services/orders.ts +300 -0
- package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +67 -0
- package/apps/storefront/src/lib/admin/viewmodels/customers.ts +56 -0
- package/apps/storefront/src/lib/admin/viewmodels/fulfillment.ts +49 -0
- package/apps/storefront/src/lib/admin/viewmodels/orders.ts +92 -0
- package/apps/storefront/src/lib/admin/viewmodels/products.ts +74 -0
- package/apps/storefront/src/lib/admin/viewmodels/reporting.ts +34 -0
- package/apps/storefront/src/lib/admin/viewmodels/settings.ts +64 -0
- package/apps/storefront/src/lib/adminTenantEnvelope.ts +90 -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 +119 -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 +244 -0
- package/apps/storefront/src/lib/chrome/parseConfig.ts +34 -0
- package/apps/storefront/src/lib/colors.ts +97 -0
- package/apps/storefront/src/lib/commerce/foxyCartSettings.ts +253 -0
- package/apps/storefront/src/lib/content-edit/applyDiff.ts +91 -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 +366 -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/activityIngestToken.ts +78 -0
- package/apps/storefront/src/lib/dev/activityStore.ts +72 -0
- package/apps/storefront/src/lib/dev/ai-widget-client.js +308 -0
- package/apps/storefront/src/lib/dev/apiBase.ts +14 -0
- package/apps/storefront/src/lib/dev/cliSignInCode.ts +155 -0
- package/apps/storefront/src/lib/dev/devDraft.ts +133 -0
- package/apps/storefront/src/lib/dev/hostedCockpit.ts +169 -0
- package/apps/storefront/src/lib/dev/livePreview.ts +77 -0
- package/apps/storefront/src/lib/dev/runtimeStore.ts +120 -0
- package/apps/storefront/src/lib/dev/storeStatus.ts +142 -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 +38 -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 +166 -0
- package/apps/storefront/src/lib/maintenance.ts +71 -0
- package/apps/storefront/src/lib/membership/model.ts +97 -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/priceBuckets.ts +33 -0
- package/apps/storefront/src/lib/rawChrome.ts +505 -0
- package/apps/storefront/src/lib/rawMarketingHtml.ts +136 -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 +607 -0
- package/apps/storefront/src/lib/subscription/model.ts +68 -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 +249 -0
- package/apps/storefront/src/lib/tot/totClientInterface.ts +30 -0
- package/apps/storefront/src/lib/url.ts +142 -0
- package/apps/storefront/src/lib/wishlist.ts +63 -0
- package/apps/storefront/src/middleware/index.ts +455 -0
- package/apps/storefront/src/pages/404.astro +61 -0
- package/apps/storefront/src/pages/[...slug].astro +90 -0
- package/apps/storefront/src/pages/admin.astro +1675 -0
- package/apps/storefront/src/pages/api/admin/orders/[id].ts +53 -0
- package/apps/storefront/src/pages/api/admin/orders.ts +71 -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/checkout-style.css.ts +43 -0
- package/apps/storefront/src/pages/api/cli/policy.ts +70 -0
- package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +164 -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/activity.ts +124 -0
- package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +73 -0
- package/apps/storefront/src/pages/api/dev/diff.ts +68 -0
- package/apps/storefront/src/pages/api/dev/file.ts +57 -0
- package/apps/storefront/src/pages/api/dev/locate.ts +125 -0
- package/apps/storefront/src/pages/api/dev/publish.ts +128 -0
- package/apps/storefront/src/pages/api/dev/tree.ts +70 -0
- package/apps/storefront/src/pages/api/dev/write.ts +116 -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 +177 -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/index.astro +290 -0
- package/apps/storefront/src/pages/llms.txt.ts +92 -0
- package/apps/storefront/src/pages/products/[handle].astro +402 -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 +1280 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +229 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +144 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +116 -0
- package/apps/storefront/src/pages/style-guide/index.astro +98 -0
- package/apps/storefront/src/pages/tenants/[id]/[...path].ts +57 -0
- package/apps/storefront/src/styles/global.css +292 -0
- package/apps/storefront/src/themes/reference.ts +68 -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/package.json +26 -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/checkout-style-publish.ts +200 -0
- package/packages/public-runtime/src/checkout-style.ts +375 -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-orders.ts +136 -0
- package/packages/public-runtime/src/foxy.ts +196 -0
- package/packages/public-runtime/src/hot-standby-readiness.ts +140 -0
- package/packages/public-runtime/src/index.ts +34 -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 +384 -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/dev/ai-edit.mjs +183 -0
- package/scripts/dev/checkout-watch.mjs +116 -0
- package/scripts/dev/file-browser.mjs +159 -0
- package/scripts/dev/file-writer.mjs +141 -0
- package/scripts/dev/git-status.mjs +171 -0
- package/scripts/dev/locate-handler.mjs +163 -0
- package/scripts/dev/locate.mjs +112 -0
- package/scripts/dev/port-check.mjs +48 -0
- package/scripts/dev/safe-path.mjs +109 -0
- package/scripts/dev/shot.mjs +46 -0
- package/scripts/dev/tenant-source-root.mjs +76 -0
- package/scripts/dev/unified-diff.mjs +81 -0
- package/scripts/tot-dev.mjs +376 -0
- package/tenants/README.md +14 -0
- package/tenants/home/content/chrome.json +7 -0
- package/tenants/home/content/home.html +58 -0
- package/tenants/home/content/home.json +11 -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/tsconfig.base.json +20 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant-resolver factory. Picks the backing store at request time:
|
|
3
|
+
*
|
|
4
|
+
* - TENANT_CACHE KV bound (prod / wired dev) -> KvTenantResolver over real KV.
|
|
5
|
+
* - No KV binding (plain `astro dev`) -> KvTenantResolver over an
|
|
6
|
+
* in-memory KV seeded from the static registry, so the dynamic path and a
|
|
7
|
+
* data-driven default are exercised locally without a Cloudflare account.
|
|
8
|
+
*
|
|
9
|
+
* Either way the caller gets a TenantResolver and nothing downstream changes.
|
|
10
|
+
* The static registry is passed as a fallback so unseeded hosts still resolve —
|
|
11
|
+
* and in DEV that fallback ALSO covers the local dev loop's own checked-out
|
|
12
|
+
* tenant (see devFallbackResolver below). Note the @astrojs/cloudflare dev
|
|
13
|
+
* runtime proxies a (local, empty) TENANT_CACHE KV from wrangler.toml, so plain
|
|
14
|
+
* `astro dev` usually takes the KV-bound branch and resolves static/dev tenants
|
|
15
|
+
* through this shared FALLBACK — not the seeded MemoryKv.
|
|
16
|
+
*
|
|
17
|
+
* The unknown-host default is data-driven: in prod it's the KV
|
|
18
|
+
* `config:default_tenant` key (set via putTenant({makeDefault:true})); in dev
|
|
19
|
+
* it's seeded from DEFAULT_TENANT_ID (falling back to the first static tenant).
|
|
20
|
+
* Changing that value flips which tenant a bare/unknown host serves — proof the
|
|
21
|
+
* default is configurable, not hardcoded.
|
|
22
|
+
*/
|
|
23
|
+
import type { TenantResolver } from "@tot/public-runtime";
|
|
24
|
+
import { readEnv, readKv } from "@/lib/env";
|
|
25
|
+
import {
|
|
26
|
+
tenantResolver as staticResolver,
|
|
27
|
+
staticTenants,
|
|
28
|
+
StaticTenantResolver,
|
|
29
|
+
} from "@/config/tenants";
|
|
30
|
+
import { KvTenantResolver, putTenant } from "@/config/kvTenantResolver";
|
|
31
|
+
import { getTenantTheme } from "@/themes/tenants";
|
|
32
|
+
import { devTenantsToSeed } from "@/config/devTenantSeed";
|
|
33
|
+
import { MemoryKv } from "@/lib/memoryKv";
|
|
34
|
+
|
|
35
|
+
// LOCAL dev loop discovery: enumerate the colocated `tenants/<id>/` dirs on disk,
|
|
36
|
+
// INCLUDING a developer's checkout grafted in by `tot dev` (scripts/tot-dev.mjs).
|
|
37
|
+
// Keys only — the loaders are never invoked. Vite resolves this glob at
|
|
38
|
+
// dev-server boot (after the graft) and re-resolves on HMR, so a newly grafted
|
|
39
|
+
// tenant dir is discovered. Mirrors the content/theme dir globs the app already
|
|
40
|
+
// uses (provider.ts, themes/tenants/index.ts). Depth: from src/config/ the repo
|
|
41
|
+
// root is four levels up (config→src→storefront→apps→root).
|
|
42
|
+
const localTenantFiles = import.meta.glob([
|
|
43
|
+
"../../../../tenants/*/theme.json",
|
|
44
|
+
"../../../../tenants/*/content/home.html",
|
|
45
|
+
"../../../../tenants/*/content/home.json",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/** Dir names under `tenants/*` (dir == tenant id), from the discovery glob keys. */
|
|
49
|
+
function localTenantDirIds(): string[] {
|
|
50
|
+
const ids = new Set<string>();
|
|
51
|
+
for (const key of Object.keys(localTenantFiles)) {
|
|
52
|
+
const id = /\/tenants\/([^/]+)\//.exec(key)?.[1];
|
|
53
|
+
if (id) ids.add(id);
|
|
54
|
+
}
|
|
55
|
+
return [...ids];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The fallback resolver consulted on a KV miss. In PROD it's the plain static
|
|
60
|
+
* registry. In DEV it's the static registry EXTENDED with a synthesized,
|
|
61
|
+
* servable tenant for each local `tenants/<id>/` dir that isn't already static —
|
|
62
|
+
* so a developer's OWN checked-out tenant (grafted by `tot dev`, never in
|
|
63
|
+
* staticTenants) resolves at `/<appDomain>/` from its checkout instead of
|
|
64
|
+
* 404ing. Static tenants keep their richer hardcoded config (they're skipped by
|
|
65
|
+
* devTenantsToSeed). Used as the fallback in BOTH resolver branches, so it works
|
|
66
|
+
* whether or not the dev runtime binds a local TENANT_CACHE KV.
|
|
67
|
+
*/
|
|
68
|
+
function devFallbackResolver(): TenantResolver {
|
|
69
|
+
if (!import.meta.env.DEV) return staticResolver;
|
|
70
|
+
const staticIds = new Set(staticTenants.map((t) => t.tenant_id));
|
|
71
|
+
const dev = devTenantsToSeed(localTenantDirIds(), staticIds, getTenantTheme);
|
|
72
|
+
if (dev.length === 0) return staticResolver;
|
|
73
|
+
// registry[0] stays the static default (HOME_TENANT) for unknown-host fallback.
|
|
74
|
+
return new StaticTenantResolver([...staticTenants, ...dev]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let devKvPromise: Promise<KVNamespace> | null = null;
|
|
78
|
+
|
|
79
|
+
/** Build (once) an in-memory KV seeded from the static registry for dev. */
|
|
80
|
+
function devKv(): Promise<KVNamespace> {
|
|
81
|
+
if (!devKvPromise) {
|
|
82
|
+
devKvPromise = (async () => {
|
|
83
|
+
const kv = new MemoryKv();
|
|
84
|
+
const configured = (await readEnv("DEFAULT_TENANT_ID"))?.trim();
|
|
85
|
+
const defaultId = configured || staticTenants[0]?.tenant_id;
|
|
86
|
+
for (const tenant of staticTenants) {
|
|
87
|
+
await putTenant(kv, tenant, { makeDefault: tenant.tenant_id === defaultId });
|
|
88
|
+
}
|
|
89
|
+
return kv;
|
|
90
|
+
})();
|
|
91
|
+
}
|
|
92
|
+
return devKvPromise;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function getTenantResolver(): Promise<TenantResolver> {
|
|
96
|
+
// Synthesized dev tenants live in the shared fallback, so both branches resolve
|
|
97
|
+
// the dev loop's checked-out tenant on a KV miss (prod fallback is unchanged).
|
|
98
|
+
const fallback = devFallbackResolver();
|
|
99
|
+
const kv = await readKv("TENANT_CACHE");
|
|
100
|
+
if (kv) return new KvTenantResolver(kv, fallback);
|
|
101
|
+
return new KvTenantResolver(await devKv(), fallback);
|
|
102
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static tenant registry — SANITIZED for the world-shareable storefront runner.
|
|
3
|
+
*
|
|
4
|
+
* This file is REWRITTEN at build time (scripts/build/build-runner.mjs) so the
|
|
5
|
+
* public npm runner ships NO real merchant roster. It carries only a neutral
|
|
6
|
+
* "home" default and one GENERIC sample tenant (with a compliance block, so the
|
|
7
|
+
* age-gate / PACT affordance is demonstrable off a non-merchant identity).
|
|
8
|
+
*
|
|
9
|
+
* At runtime the dev loop's checkout-glob resolver (config/resolver.ts) synthesizes
|
|
10
|
+
* a servable tenant for whatever the developer grafts into tenants/<id>/, so the
|
|
11
|
+
* real staticTenants are not needed here. The public API (staticTenants,
|
|
12
|
+
* StaticTenantResolver, tenantResolver) is unchanged — only the data is generic.
|
|
13
|
+
*/
|
|
14
|
+
import type { TenantConfig, TenantResolver } from "@tot/public-runtime";
|
|
15
|
+
import { getTenantTheme } from "@/themes/tenants";
|
|
16
|
+
|
|
17
|
+
// Default landing tenant — the fallback for unmatched hosts / bare localhost.
|
|
18
|
+
// Non-commerce "coming soon" placeholder; never a fake merchant. Registered first
|
|
19
|
+
// so it is the resolver default (registry[0]).
|
|
20
|
+
export const HOME_TENANT: TenantConfig = {
|
|
21
|
+
tenant_id: "home",
|
|
22
|
+
appDomain: "home",
|
|
23
|
+
// NB: do NOT claim "localhost" here — a bare localhost host must fall THROUGH to
|
|
24
|
+
// path-prefix routing (/<domain>/…) so grafted dev tenants + the sample resolve.
|
|
25
|
+
// Unknown/bare hosts still land on HOME via resolveByHost (registry[0]).
|
|
26
|
+
domains: ["home"],
|
|
27
|
+
canonicalDomain: "home",
|
|
28
|
+
displayName: "Storefront",
|
|
29
|
+
storyblok_space_token: "STORYBLOK_HOME_DELIVERY_TOKEN_PLACEHOLDER",
|
|
30
|
+
tot_data_scope: "home",
|
|
31
|
+
currency: "USD",
|
|
32
|
+
locale: "en-US",
|
|
33
|
+
theme_tokens: getTenantTheme("home"),
|
|
34
|
+
siteType: "marketing",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Generic SAMPLE tenant — a placeholder regulated store used to demonstrate the
|
|
38
|
+
// compliance affordance (age-gate + nicotine warning) WITHOUT any real merchant.
|
|
39
|
+
// Routed at /sample-store.example/. Content comes from the developer's grafted
|
|
40
|
+
// checkout (tenants/sample-store.example/); with none present it degrades to the
|
|
41
|
+
// default scope, and the compliance overlay still renders from this config.
|
|
42
|
+
export const SAMPLE_TENANT: TenantConfig = {
|
|
43
|
+
tenant_id: "sample-store.example",
|
|
44
|
+
appDomain: "sample-store.example",
|
|
45
|
+
domains: ["sample-store.example"],
|
|
46
|
+
canonicalDomain: "sample-store.example",
|
|
47
|
+
displayName: "Sample Store",
|
|
48
|
+
storyblok_space_token: "STORYBLOK_SAMPLE_DELIVERY_TOKEN_PLACEHOLDER",
|
|
49
|
+
tot_data_scope: "sample-store.example",
|
|
50
|
+
currency: "USD",
|
|
51
|
+
locale: "en-US",
|
|
52
|
+
theme_tokens: getTenantTheme("sample-store.example"),
|
|
53
|
+
// Regulated-store demo: shows the 21+/PACT compliance gate is CONFIG-driven.
|
|
54
|
+
compliance: {
|
|
55
|
+
minAge: 21,
|
|
56
|
+
nicotineWarning: true,
|
|
57
|
+
shippingRestriction:
|
|
58
|
+
"Adult signature (21+) required on delivery. Some destinations are restricted — verified at checkout.",
|
|
59
|
+
},
|
|
60
|
+
siteType: "marketing",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const REGISTRY: TenantConfig[] = [HOME_TENANT, SAMPLE_TENANT];
|
|
64
|
+
|
|
65
|
+
/** The built-in tenants — seed source for the KV store (dev MemoryKv / prod). */
|
|
66
|
+
export const staticTenants: TenantConfig[] = REGISTRY;
|
|
67
|
+
|
|
68
|
+
export class StaticTenantResolver implements TenantResolver {
|
|
69
|
+
constructor(private readonly registry: TenantConfig[] = REGISTRY) {}
|
|
70
|
+
|
|
71
|
+
/** Strict domain match — null when no tenant explicitly claims `host`. */
|
|
72
|
+
async resolveStrict(host: string): Promise<TenantConfig | null> {
|
|
73
|
+
const clean = host.toLowerCase().split(":")[0]?.trim() ?? "";
|
|
74
|
+
if (!clean) return null;
|
|
75
|
+
return (
|
|
76
|
+
this.registry.find((t) => t.domains.some((d) => d.toLowerCase() === clean)) ?? null
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async resolveByHost(host: string): Promise<TenantConfig | null> {
|
|
81
|
+
const match = await this.resolveStrict(host);
|
|
82
|
+
// Unmatched hosts / bare localhost fall back to the default HOME_TENANT.
|
|
83
|
+
return match ?? this.registry[0] ?? null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const tenantResolver: TenantResolver = new StaticTenantResolver();
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Root layout. Injects per-tenant theme CSS vars at the edge, loads the font
|
|
4
|
+
* pairing, mounts header + footer chrome, and wires privacy-friendly analytics.
|
|
5
|
+
* Every page renders inside this.
|
|
6
|
+
*/
|
|
7
|
+
import "@/styles/global.css";
|
|
8
|
+
import Seo from "@/components/Seo.astro";
|
|
9
|
+
import AnnouncementBar from "@/components/chrome/AnnouncementBar.astro";
|
|
10
|
+
import SiteHeader from "@/components/chrome/SiteHeader.astro";
|
|
11
|
+
import SiteFooter from "@/components/chrome/SiteFooter.astro";
|
|
12
|
+
import QuickView from "@/components/islands/QuickView.tsx";
|
|
13
|
+
import FoxyLoader from "@/components/commerce/FoxyLoader.astro";
|
|
14
|
+
import FoxySidecartStyles from "@/components/commerce/FoxySidecartStyles.astro";
|
|
15
|
+
import NicotineWarning from "@/components/compliance/NicotineWarning.astro";
|
|
16
|
+
import ShippingRestrictionNotice from "@/components/compliance/ShippingRestrictionNotice.astro";
|
|
17
|
+
import IsolatedAgeGate from "@/components/islands/IsolatedAgeGate.tsx";
|
|
18
|
+
import AuthBadge from "@/components/auth/AuthBadge.astro";
|
|
19
|
+
import { referenceTheme } from "@/themes/reference";
|
|
20
|
+
import { mergeTheme, themeFontHref } from "@/themes/schema";
|
|
21
|
+
import { organizationLd, websiteLd } from "@/lib/jsonld";
|
|
22
|
+
import { nonceInlineTags, CHROME_BODY_MARKER } from "@/lib/rawMarketingHtml";
|
|
23
|
+
import { readViewerSession } from "@/lib/auth/route";
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
title: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
path: string;
|
|
29
|
+
ogType?: "website" | "product" | "article";
|
|
30
|
+
ogImage?: string;
|
|
31
|
+
noindex?: boolean;
|
|
32
|
+
jsonLd?: Record<string, unknown> | Record<string, unknown>[];
|
|
33
|
+
/** Wider pages (PLP/home) drop the prose max-width. */
|
|
34
|
+
fullBleed?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { title, description, path, ogType, ogImage, noindex, jsonLd } =
|
|
38
|
+
Astro.props;
|
|
39
|
+
const { tenant, themeStyle, canonicalBase, basePath, cspNonce, features, capabilities } =
|
|
40
|
+
Astro.locals;
|
|
41
|
+
|
|
42
|
+
// Organization JSON-LD is emitted site-wide (brief §8); pages add their own
|
|
43
|
+
// graphs (Product, BreadcrumbList…) via the jsonLd prop.
|
|
44
|
+
const pageGraphs = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : [];
|
|
45
|
+
const allJsonLd = [
|
|
46
|
+
organizationLd(tenant, canonicalBase),
|
|
47
|
+
websiteLd(tenant, canonicalBase),
|
|
48
|
+
...pageGraphs,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const theme = mergeTheme(referenceTheme, tenant.theme_tokens);
|
|
52
|
+
const fontHref = themeFontHref(theme);
|
|
53
|
+
|
|
54
|
+
const chrome = await Astro.locals.content.getChrome();
|
|
55
|
+
|
|
56
|
+
// SHARED CHROME: if the tenant ships content/<id>/chrome.html (the pixel-perfect
|
|
57
|
+
// header/footer the raw marketing pages use), render THAT chrome around the
|
|
58
|
+
// Layout content too — so commerce pages (collections/products/search/…) share
|
|
59
|
+
// ONE chrome instead of the generic Header/Footer. Split the wrapper at
|
|
60
|
+
// <!--PAGE_BODY-->: everything before = header, after = footer; pull its tenant
|
|
61
|
+
// stylesheet links into <head>; nonce its inline tags for CSP. Tenants without a
|
|
62
|
+
// chrome.html keep the token-driven Header/Footer (below).
|
|
63
|
+
const chromeHtml = await Astro.locals.content.getChromeHtml();
|
|
64
|
+
let sharedChrome: { header: string; footer: string; css: string[] } | null = null;
|
|
65
|
+
if (chromeHtml && chromeHtml.includes(CHROME_BODY_MARKER)) {
|
|
66
|
+
const headEnd = chromeHtml.indexOf("</head>");
|
|
67
|
+
const head = headEnd >= 0 ? chromeHtml.slice(0, headEnd) : "";
|
|
68
|
+
const css = [...head.matchAll(/<link[^>]+href="([^"]+\.css)"[^>]*>/gi)]
|
|
69
|
+
.map((m) => m[1] ?? "")
|
|
70
|
+
.filter((h) => h.startsWith("/tenants/") || h.startsWith("/shared/"));
|
|
71
|
+
const bodyOpen = chromeHtml.indexOf(">", chromeHtml.indexOf("<body")) + 1;
|
|
72
|
+
const marker = chromeHtml.indexOf(CHROME_BODY_MARKER);
|
|
73
|
+
const bodyEnd = chromeHtml.lastIndexOf("</body>");
|
|
74
|
+
sharedChrome = {
|
|
75
|
+
header: nonceInlineTags(chromeHtml.slice(bodyOpen, marker), cspNonce),
|
|
76
|
+
footer: nonceInlineTags(chromeHtml.slice(marker + CHROME_BODY_MARKER.length, bodyEnd >= 0 ? bodyEnd : undefined), cspNonce),
|
|
77
|
+
css,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const compliance = tenant.compliance;
|
|
82
|
+
|
|
83
|
+
// Site-chrome data resolution (unit I4 — chrome/* is the canonical, prop-driven
|
|
84
|
+
// component set; this Layout is the ONE place that resolves session/feature/
|
|
85
|
+
// theme state into their props. Skipped entirely when sharedChrome (a raw
|
|
86
|
+
// content/<id>/chrome.html override) is present — that path wins outright.
|
|
87
|
+
//
|
|
88
|
+
// Auth entry point: "Dashboard" when a session exists, "Sign in" otherwise.
|
|
89
|
+
// Without this there's no discoverable path from the storefront into sign-in /
|
|
90
|
+
// the vendor dashboard (#8425). On login-gated hosts the middleware already
|
|
91
|
+
// resolved the viewer (locals.viewer); reuse it and only read the session KV on
|
|
92
|
+
// open hosts where the gate never ran. Signed-out "Sign in" carries the current
|
|
93
|
+
// path as `next`, so after the email-code round-trip the visitor lands back
|
|
94
|
+
// where they were instead of the site root.
|
|
95
|
+
const viewer = Astro.locals.viewer ?? (await readViewerSession(Astro));
|
|
96
|
+
const nextParam = encodeURIComponent(Astro.url.pathname + Astro.url.search);
|
|
97
|
+
const authLink = {
|
|
98
|
+
label: viewer ? "Dashboard" : "Sign in",
|
|
99
|
+
href: viewer ? "/dashboard" : `/auth/login?next=${nextParam}`,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Per-vendor header treatment from the theme file: a "dark" header renders the
|
|
103
|
+
// bar in the primary color carrying the light logo (e.g. Giant Vapes' navy +
|
|
104
|
+
// white logo); a light header always shows the text wordmark (no logo image).
|
|
105
|
+
const headerDark = theme.brand.headerVariant === "dark";
|
|
106
|
+
const headerLogoSrc = headerDark ? theme.brand.logoLight : theme.brand.logoDark;
|
|
107
|
+
const headerLogo =
|
|
108
|
+
headerDark && headerLogoSrc && /^(https?:|\/)/.test(headerLogoSrc)
|
|
109
|
+
? { src: headerLogoSrc, alt: tenant.displayName }
|
|
110
|
+
: undefined;
|
|
111
|
+
|
|
112
|
+
// Footer bottom bar: a "Legal"-titled column's links win; otherwise every
|
|
113
|
+
// column's links flatten into the policy-link row (mirrors the retired
|
|
114
|
+
// nav/Footer.astro exactly).
|
|
115
|
+
const legalCol = chrome.footer.columns.find((c) => /legal/i.test(c.title));
|
|
116
|
+
const bottomLinks = legalCol ? legalCol.links : chrome.footer.columns.flatMap((c) => c.links);
|
|
117
|
+
const legalLine = `© ${new Date().getFullYear()} ${tenant.displayName}${
|
|
118
|
+
chrome.footer.legal ? ` · ${chrome.footer.legal}` : ""
|
|
119
|
+
}`;
|
|
120
|
+
|
|
121
|
+
const analyticsToken = import.meta.env.PUBLIC_CF_ANALYTICS_TOKEN;
|
|
122
|
+
const analyticsEnabled =
|
|
123
|
+
analyticsToken && !/REPLACE|PLACEHOLDER/i.test(analyticsToken);
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
<!doctype html>
|
|
127
|
+
<html lang={tenant.locale.split("-")[0] ?? "en"} data-base-path={basePath || undefined}>
|
|
128
|
+
<head>
|
|
129
|
+
<meta charset="utf-8" />
|
|
130
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
131
|
+
<link rel="icon" type="image/svg+xml" href={theme.brand.favicon} />
|
|
132
|
+
|
|
133
|
+
{/* Per-tenant theme tokens injected at the edge (brief §7). */}
|
|
134
|
+
<style set:html={themeStyle} nonce={cspNonce}></style>
|
|
135
|
+
|
|
136
|
+
{/* Shared chrome's own stylesheet(s) — the tenant theme (/tenants/...) plus
|
|
137
|
+
either the tenant's own mkt.css or the platform /shared/commerce-chrome.css
|
|
138
|
+
— so the ported chrome renders in-brand on Layout (commerce) pages too. */}
|
|
139
|
+
{sharedChrome?.css.map((href) => <link rel="stylesheet" href={href} />)}
|
|
140
|
+
|
|
141
|
+
{
|
|
142
|
+
fontHref && (
|
|
143
|
+
<>
|
|
144
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
145
|
+
<link
|
|
146
|
+
rel="preconnect"
|
|
147
|
+
href="https://fonts.gstatic.com"
|
|
148
|
+
crossorigin
|
|
149
|
+
/>
|
|
150
|
+
<link rel="stylesheet" href={fontHref} />
|
|
151
|
+
</>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
<Seo
|
|
156
|
+
title={title}
|
|
157
|
+
description={description}
|
|
158
|
+
path={path}
|
|
159
|
+
ogType={ogType}
|
|
160
|
+
ogImage={ogImage}
|
|
161
|
+
noindex={noindex}
|
|
162
|
+
jsonLd={allJsonLd}
|
|
163
|
+
/>
|
|
164
|
+
|
|
165
|
+
{
|
|
166
|
+
analyticsEnabled && (
|
|
167
|
+
<script
|
|
168
|
+
defer
|
|
169
|
+
src="https://static.cloudflareinsights.com/beacon.min.js"
|
|
170
|
+
data-cf-beacon={`{"token": "${analyticsToken}"}`}
|
|
171
|
+
/>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
</head>
|
|
175
|
+
<body>
|
|
176
|
+
<a
|
|
177
|
+
href="#main"
|
|
178
|
+
class="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:m-3 focus:rounded-md focus:bg-[var(--color-primary)] focus:px-4 focus:py-2 focus:text-[var(--color-primary-contrast)]"
|
|
179
|
+
>
|
|
180
|
+
Skip to content
|
|
181
|
+
</a>
|
|
182
|
+
|
|
183
|
+
<AuthBadge />
|
|
184
|
+
|
|
185
|
+
{sharedChrome ? (
|
|
186
|
+
<Fragment set:html={sharedChrome.header} />
|
|
187
|
+
) : (
|
|
188
|
+
<>
|
|
189
|
+
{compliance?.nicotineWarning && <NicotineWarning variant="bar" />}
|
|
190
|
+
<div class="sticky top-0 z-40">
|
|
191
|
+
{chrome.announcement && <AnnouncementBar text={chrome.announcement} variant="static" />}
|
|
192
|
+
<SiteHeader
|
|
193
|
+
logo={headerLogo}
|
|
194
|
+
wordmark={tenant.displayName}
|
|
195
|
+
nav={chrome.nav}
|
|
196
|
+
variant={headerDark ? "dark" : "light"}
|
|
197
|
+
announcement={chrome.announcement}
|
|
198
|
+
searchAction={features.productSearch ? "/search" : undefined}
|
|
199
|
+
savedItemsHref={features.savedItems ? "/saved" : undefined}
|
|
200
|
+
headerCtas={chrome.headerCtas}
|
|
201
|
+
authLink={authLink}
|
|
202
|
+
sticky={false}
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
</>
|
|
206
|
+
)}
|
|
207
|
+
|
|
208
|
+
<main id="main">
|
|
209
|
+
<slot />
|
|
210
|
+
</main>
|
|
211
|
+
|
|
212
|
+
{sharedChrome ? (
|
|
213
|
+
<Fragment set:html={sharedChrome.footer} />
|
|
214
|
+
) : (
|
|
215
|
+
<>
|
|
216
|
+
<ShippingRestrictionNotice text={compliance?.shippingRestriction} />
|
|
217
|
+
<SiteFooter
|
|
218
|
+
brand={{
|
|
219
|
+
wordmark: tenant.displayName,
|
|
220
|
+
contact: chrome.footer.tagline ? [chrome.footer.tagline] : undefined,
|
|
221
|
+
}}
|
|
222
|
+
columns={chrome.footer.columns}
|
|
223
|
+
newsletter={features.newsletter ? { title: "Newsletter", placeholder: "you@example.com" } : undefined}
|
|
224
|
+
social={chrome.footer.social}
|
|
225
|
+
legal={legalLine}
|
|
226
|
+
bottomLinks={bottomLinks}
|
|
227
|
+
/>
|
|
228
|
+
</>
|
|
229
|
+
)}
|
|
230
|
+
|
|
231
|
+
{capabilities.ageVerification.enabled && compliance?.minAge && (
|
|
232
|
+
<IsolatedAgeGate client:load minAge={compliance.minAge} brand={tenant.displayName} />
|
|
233
|
+
)}
|
|
234
|
+
|
|
235
|
+
{/* Global quick-view dialog (opened by any [data-quickview] card button).
|
|
236
|
+
Commerce-only — marketing tenants ship no product UI. */}
|
|
237
|
+
{features.quickView && <QuickView client:idle locale={tenant.locale} />}
|
|
238
|
+
|
|
239
|
+
{/* Excise tax (Track B) — when enabled for this environment, the excise line
|
|
240
|
+
is calculated + added inside the FoxyCart cart (tot-foxycart pre-cart
|
|
241
|
+
webhook). This is the storefront's honest shopper-facing signal of it. */}
|
|
242
|
+
{capabilities.exciseTax.enabled && features.catalog && (
|
|
243
|
+
<p
|
|
244
|
+
class="border-t border-[var(--color-border)] bg-[var(--color-bg)] py-2.5 text-center text-xs text-[var(--color-muted)]"
|
|
245
|
+
data-capability="excise-tax"
|
|
246
|
+
>
|
|
247
|
+
Applicable excise tax is calculated at checkout.
|
|
248
|
+
</p>
|
|
249
|
+
)}
|
|
250
|
+
|
|
251
|
+
{/* FoxyCart loader (Powered by FoxyCart) — turns add-to-cart forms into the
|
|
252
|
+
sidecart + hosted checkout. Only for commerce tenants with a store. */}
|
|
253
|
+
<FoxyLoader />
|
|
254
|
+
|
|
255
|
+
{/* Brand Foxy's in-page sidecart to the tenant theme (baseline + optional
|
|
256
|
+
per-tenant commerce.foxy.sidecartCss override). Commerce tenants only. */}
|
|
257
|
+
<FoxySidecartStyles />
|
|
258
|
+
|
|
259
|
+
{/* Wishlist hearts: event-delegated toggle + state hydration + count sync.
|
|
260
|
+
Cards/PDP carry [data-wishlist]; the header shows [data-wishlist-count].
|
|
261
|
+
Kept as one tiny script (no per-card hydration). Commerce-only — a
|
|
262
|
+
marketing tenant has no saved-items surface, so this never renders. */}
|
|
263
|
+
{features.savedItems && (
|
|
264
|
+
<script>
|
|
265
|
+
import {
|
|
266
|
+
readWishlist,
|
|
267
|
+
toggleSaved,
|
|
268
|
+
isSaved,
|
|
269
|
+
WISHLIST_EVENT,
|
|
270
|
+
} from "@/lib/wishlist";
|
|
271
|
+
|
|
272
|
+
function syncCount() {
|
|
273
|
+
const n = readWishlist().length;
|
|
274
|
+
document.querySelectorAll<HTMLElement>("[data-wishlist-count]").forEach((el) => {
|
|
275
|
+
el.textContent = n > 0 ? String(n) : "";
|
|
276
|
+
el.toggleAttribute("hidden", n === 0);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function syncHearts() {
|
|
281
|
+
document.querySelectorAll<HTMLButtonElement>(".wishlist-toggle").forEach((btn) => {
|
|
282
|
+
try {
|
|
283
|
+
const { handle } = JSON.parse(btn.getAttribute("data-wishlist") || "{}");
|
|
284
|
+
if (handle) btn.setAttribute("aria-pressed", String(isSaved(handle)));
|
|
285
|
+
} catch {}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
document.addEventListener("click", (e) => {
|
|
290
|
+
const btn = (e.target as HTMLElement)?.closest?.<HTMLButtonElement>(".wishlist-toggle");
|
|
291
|
+
if (!btn) return;
|
|
292
|
+
e.preventDefault();
|
|
293
|
+
try {
|
|
294
|
+
const item = JSON.parse(btn.getAttribute("data-wishlist") || "{}");
|
|
295
|
+
if (!item.handle) return;
|
|
296
|
+
const saved = toggleSaved(item);
|
|
297
|
+
btn.setAttribute("aria-pressed", String(saved));
|
|
298
|
+
} catch {}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
window.addEventListener(WISHLIST_EVENT, syncCount);
|
|
302
|
+
syncHearts();
|
|
303
|
+
syncCount();
|
|
304
|
+
</script>
|
|
305
|
+
)}
|
|
306
|
+
|
|
307
|
+
{/* Path-prefix tenant routing: when this request was selected via a
|
|
308
|
+
/<domain>/ path segment (locals.basePath), keep same-origin
|
|
309
|
+
root-absolute links inside that prefix so navigation stays on the
|
|
310
|
+
tenant. Host-routed requests have no base path and skip this entirely. */}
|
|
311
|
+
{basePath && (
|
|
312
|
+
<script is:inline nonce={cspNonce} define:vars={{ basePath }}>
|
|
313
|
+
(function () {
|
|
314
|
+
var base = basePath;
|
|
315
|
+
if (!base) return;
|
|
316
|
+
function fix(a) {
|
|
317
|
+
var href = a.getAttribute("href");
|
|
318
|
+
if (!href || href.charAt(0) !== "/" || href.charAt(1) === "/") return;
|
|
319
|
+
if (href === base || href.indexOf(base + "/") === 0) return;
|
|
320
|
+
a.setAttribute("href", base + href);
|
|
321
|
+
}
|
|
322
|
+
function fixAll() {
|
|
323
|
+
document.querySelectorAll('a[href^="/"]').forEach(fix);
|
|
324
|
+
}
|
|
325
|
+
if (document.readyState === "loading") {
|
|
326
|
+
document.addEventListener("DOMContentLoaded", fixAll);
|
|
327
|
+
} else {
|
|
328
|
+
fixAll();
|
|
329
|
+
}
|
|
330
|
+
// Late/island-rendered anchors: fix just-in-time before navigation.
|
|
331
|
+
document.addEventListener(
|
|
332
|
+
"pointerdown",
|
|
333
|
+
function (e) {
|
|
334
|
+
var a = e.target && e.target.closest && e.target.closest('a[href^="/"]');
|
|
335
|
+
if (a) fix(a);
|
|
336
|
+
},
|
|
337
|
+
true,
|
|
338
|
+
);
|
|
339
|
+
})();
|
|
340
|
+
</script>
|
|
341
|
+
)}
|
|
342
|
+
|
|
343
|
+
{/* Scroll-reveal + analytics pageview hook. ~0.5KB, no framework. */}
|
|
344
|
+
<script>
|
|
345
|
+
// Reveal-on-scroll (respects reduced motion via CSS).
|
|
346
|
+
const els = document.querySelectorAll("[data-reveal]");
|
|
347
|
+
if (
|
|
348
|
+
els.length &&
|
|
349
|
+
!window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
350
|
+
) {
|
|
351
|
+
const io = new IntersectionObserver(
|
|
352
|
+
(entries) => {
|
|
353
|
+
for (const e of entries) {
|
|
354
|
+
if (e.isIntersecting) {
|
|
355
|
+
e.target.classList.add("is-in");
|
|
356
|
+
io.unobserve(e.target);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
{ rootMargin: "0px 0px -8% 0px", threshold: 0.05 },
|
|
361
|
+
);
|
|
362
|
+
els.forEach((el) => io.observe(el));
|
|
363
|
+
} else {
|
|
364
|
+
els.forEach((el) => el.classList.add("is-in"));
|
|
365
|
+
}
|
|
366
|
+
</script>
|
|
367
|
+
</body>
|
|
368
|
+
</html>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# `lib/admin` — admin data/view + AI/evidence contracts
|
|
2
|
+
|
|
3
|
+
Read-oriented view models and deterministic mock services for the 7 admin
|
|
4
|
+
domains (orders, products, customers, fulfillment, reporting, settings,
|
|
5
|
+
ai-workflows), plus the AI/evidence substrate (`ai/`, `evidence/`) those
|
|
6
|
+
domains' assist surfaces build on. See
|
|
7
|
+
`docs/handoffs/admin-app-implementation-baseline.md` for the product spec
|
|
8
|
+
these implement.
|
|
9
|
+
|
|
10
|
+
## Tenant envelope
|
|
11
|
+
|
|
12
|
+
The canonical tenant envelope is `AdminTenantEnvelope`, defined in
|
|
13
|
+
`lib/adminTenantEnvelope.ts` (admin-tenant-mode) and re-exported from
|
|
14
|
+
`lib/admin/envelope.ts` so everything under `lib/admin/**` can import it from
|
|
15
|
+
a stable relative path:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
interface AdminTenantEnvelope {
|
|
19
|
+
adminAppTenant: string; // the tenant that actually served this request
|
|
20
|
+
targetTenant: string; // the merchant tenant this page's data/actions are scoped to
|
|
21
|
+
operator: string | null; // the signed-in viewer's email, or null when anonymous
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For merchant-facing admin `adminAppTenant` and `targetTenant` are equal; only
|
|
26
|
+
a real ToT-staff session may move `targetTenant` away from the routed tenant
|
|
27
|
+
(see `resolveAdminTenantMode` for the authorization rule — deny-by-default,
|
|
28
|
+
refused not hidden-only). `targetTenant` is the sole key mock services use to
|
|
29
|
+
select/derive data. **Authorization must always be evaluated against
|
|
30
|
+
`targetTenant`, never `adminAppTenant`** (decision `tenant-envelope-contract`).
|
|
31
|
+
|
|
32
|
+
(`lib/admin/envelope.ts` used to declare its own 2-field copy of this
|
|
33
|
+
interface — data-view-contracts and admin-tenant-mode were built in parallel
|
|
34
|
+
per that decision, with no live coordination. Reconciled here per decision
|
|
35
|
+
`envelope-type-duplication-follow-up`: one canonical type, re-exported.)
|
|
36
|
+
|
|
37
|
+
## Layout
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
lib/admin/
|
|
41
|
+
envelope.ts # re-exports the canonical AdminTenantEnvelope
|
|
42
|
+
seed.ts # deterministic PRNG + timestamp helpers
|
|
43
|
+
viewmodels/<domain>.ts # types only, no I/O
|
|
44
|
+
services/mock/<domain>.ts # getXViewModel(envelope) -> X ViewModel
|
|
45
|
+
services/mock/evidence.ts # getEvidenceViewModel(envelope) -> EvidenceReference[]
|
|
46
|
+
services/mock/tenants.ts # seeded tenant registry + fallback profile
|
|
47
|
+
services/mock/index.ts # barrel re-export
|
|
48
|
+
ai/
|
|
49
|
+
approvalMode.ts # canonical ApprovalMode + isDraftOnly/requiresHumanApproval/isAutomationAllowed
|
|
50
|
+
audit.ts # canonical AuditEvent (operator + targetTenant + action + at)
|
|
51
|
+
recommendation.ts # AiRecommendation — the recommendation-shaped record every AI suggestion should use
|
|
52
|
+
askEnvelope.ts # AskEnvelope + buildAskEnvelope() — the floating Ask button's context
|
|
53
|
+
evidence/
|
|
54
|
+
evidenceReference.ts # EvidenceReference + EvidenceKind — the evidence graph's typed pointer
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## AI / evidence substrate
|
|
58
|
+
|
|
59
|
+
- **Approval state machine** (`ai/approvalMode.ts`): `draft_only` |
|
|
60
|
+
`approval_required` | `guarded_automation`. `settings.ts` and
|
|
61
|
+
`aiWorkflows.ts` both import this rather than declaring their own copy.
|
|
62
|
+
- **Audit events** (`ai/audit.ts`): `{ operator, targetTenant, action, at }`.
|
|
63
|
+
`SettingsAuditEvent` extends it with a human-readable `change` string;
|
|
64
|
+
`WorkflowRecord.auditTrail` uses it directly.
|
|
65
|
+
- **Recommendations** (`ai/recommendation.ts`): `AiRecommendation` is the
|
|
66
|
+
shape every AI-suggested action should carry — `summary`, `confidence`
|
|
67
|
+
(0–1), `evidenceRefs` (ids into the evidence graph), `owner`,
|
|
68
|
+
`approvalMode`, `auditTrail`. `WorkflowRecord.suggestedAction` uses this
|
|
69
|
+
today; later per-tab assist surfaces (Order Assist, Fulfillment Assist,
|
|
70
|
+
etc.) should reuse it instead of inventing a parallel "suggestion" shape.
|
|
71
|
+
- **Evidence graph** (`evidence/evidenceReference.ts` +
|
|
72
|
+
`services/mock/evidence.ts`): a typed, tenant-scoped pointer
|
|
73
|
+
(`EvidenceReference`) connecting orders, customers, products, payments,
|
|
74
|
+
verification events, fulfillment/tracking, support/contact events, policy
|
|
75
|
+
acceptance, tax/excise evidence, and admin/user actions
|
|
76
|
+
(`EvidenceKind`). The mock service builds real edges by cross-referencing
|
|
77
|
+
the orders/customers/products mock services already in this directory
|
|
78
|
+
(an order's evidence ref links to its customer and product refs via
|
|
79
|
+
`relatedRefs`) rather than inventing disconnected fixture data.
|
|
80
|
+
- **Ask envelope** (`ai/askEnvelope.ts`): `buildAskEnvelope()` is the pure,
|
|
81
|
+
testable source of truth for what the floating Ask button emits — the full
|
|
82
|
+
tenant envelope, active tab, selected record id (if any), role +
|
|
83
|
+
permissions (derived from `isStaff`), active filters, and evidence context.
|
|
84
|
+
`admin.astro`'s `is:inline` script mirrors this logic in plain JS (inline
|
|
85
|
+
scripts can't import modules) and emits it on Ask-button click as
|
|
86
|
+
`window.__toTAdminAsk` and a `tot:admin-ask` `CustomEvent` — **dev-inspectable
|
|
87
|
+
only, no chat backend yet.** The envelope changes whenever the active tab
|
|
88
|
+
changes (kept current by `setAskContext`) and is rebuilt fresh on click so a
|
|
89
|
+
selection made via `data-admin-selected-id` (a hook future tab units can set
|
|
90
|
+
on their selected row/detail panel — none do yet) is picked up.
|
|
91
|
+
|
|
92
|
+
## Determinism
|
|
93
|
+
|
|
94
|
+
Every mock service seeds its randomness from `seededRandom(`<domain>:${targetTenant}`)`
|
|
95
|
+
(`seed.ts`), so **the same `targetTenant` always returns the same data** within
|
|
96
|
+
a process — no reliance on `Date.now()` or other wall-clock state. Two tenants
|
|
97
|
+
are curated with vertical-flavored data (`northridge-vapor.com` — vape,
|
|
98
|
+
`cascade-tobacco.com` — tobacco); any other `targetTenant` string still gets a
|
|
99
|
+
stable fallback profile via `tenantProfile()`, so callers never need to
|
|
100
|
+
special-case an "unknown" tenant.
|
|
101
|
+
|
|
102
|
+
## Usage
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { getOrdersViewModel } from "@/lib/admin/services/mock";
|
|
106
|
+
|
|
107
|
+
const orders = getOrdersViewModel({
|
|
108
|
+
adminAppTenant: tenant.tenant_id,
|
|
109
|
+
targetTenant: tenant.tenant_id,
|
|
110
|
+
operator: viewerEmail,
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Boundaries
|
|
115
|
+
|
|
116
|
+
- **Read-only.** No write/mutation paths exist yet — that is a later unit.
|
|
117
|
+
- **Tabs/pages → lib, never lib → tabs/pages.** Nothing under `lib/admin/**`
|
|
118
|
+
imports from `components/admin/**` or `pages/admin.astro`. Tab components
|
|
119
|
+
and `admin.astro` are free to import from here (`admin.astro` already does,
|
|
120
|
+
for `resolveAdminTenantMode` and the Ask-envelope logic it mirrors inline).
|
|
121
|
+
- Gaps in these contracts should go back to the workstream board as a
|
|
122
|
+
decision, not get patched from a tab's unit.
|