@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,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant configuration + theme token contract.
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 resolves tenants from a static config map/KV by hostname. The lookup
|
|
5
|
+
* interface (resolveTenant) is intentionally async + many-ready so the same seam
|
|
6
|
+
* scales to Cloudflare-for-SaaS custom hostnames later without a rewrite.
|
|
7
|
+
*/
|
|
8
|
+
import type { FoxyCommerceConfig } from "./foxy.js";
|
|
9
|
+
import type { CapabilityConfig } from "./capabilities.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The shared semantic COLOR contract — the widget library's public API.
|
|
13
|
+
*
|
|
14
|
+
* Every platform component reads ONLY these tokens (as `var(--color-<key>)`,
|
|
15
|
+
* bridged into Tailwind by `styles/global.css` `@theme`). A tenant restyles the
|
|
16
|
+
* whole library by supplying token values — or aliasing its own brand vocabulary
|
|
17
|
+
* onto them — and NEVER edits a component. `themeToCssVars()` serializes each
|
|
18
|
+
* key `k` to `--color-<k>`.
|
|
19
|
+
*
|
|
20
|
+
* The REQUIRED keys are the core palette every theme states. The OPTIONAL keys
|
|
21
|
+
* are semantic/state + interaction extensions with platform-default fallbacks in
|
|
22
|
+
* the `@theme` bridge, so `var(--color-warning)`, `var(--color-primary-hover)`,
|
|
23
|
+
* etc. always resolve even when a theme omits them; a theme sets one only to
|
|
24
|
+
* brand-tune it. Canonical reference values live in `themes/reference.ts`.
|
|
25
|
+
* Full spec + tenant-alias guidance: `docs/architecture/token-contract.md`.
|
|
26
|
+
*/
|
|
27
|
+
export interface ThemeColorTokens {
|
|
28
|
+
/** Page background. */
|
|
29
|
+
bg: string;
|
|
30
|
+
/** Raised card/panel surface. */
|
|
31
|
+
surface: string;
|
|
32
|
+
/** Primary body + heading text. */
|
|
33
|
+
text: string;
|
|
34
|
+
/** Secondary / caption text. */
|
|
35
|
+
muted: string;
|
|
36
|
+
/** Brand anchor — primary CTAs, active nav, hero fill. */
|
|
37
|
+
primary: string;
|
|
38
|
+
/** Readable text/icon color ON `primary`. */
|
|
39
|
+
"primary-contrast": string;
|
|
40
|
+
/** Single brand spark — badges, marks, large accent type. */
|
|
41
|
+
accent: string;
|
|
42
|
+
/** Hairline borders, dividers, input outlines. */
|
|
43
|
+
border: string;
|
|
44
|
+
/** Success / in-stock / savings state. */
|
|
45
|
+
success: string;
|
|
46
|
+
/** Sale / markdown price emphasis. */
|
|
47
|
+
sale: string;
|
|
48
|
+
|
|
49
|
+
// --- Optional extensions — always resolvable via the @theme bridge default;
|
|
50
|
+
// a theme sets one only to brand-tune it (see token-contract.md). ---
|
|
51
|
+
/** Subtle inset/raised fill between `bg` and `surface` (wells, table stripes). */
|
|
52
|
+
"surface-2"?: string;
|
|
53
|
+
/** Warning / low-stock state. */
|
|
54
|
+
warning?: string;
|
|
55
|
+
/** Error / out-of-stock / destructive state. */
|
|
56
|
+
error?: string;
|
|
57
|
+
/** Informational state (notices, help). */
|
|
58
|
+
info?: string;
|
|
59
|
+
/** Accent tuned for SMALL text/links on light surfaces (AA). Default: a
|
|
60
|
+
* derived darken of `accent`; set to hand-tune contrast. */
|
|
61
|
+
"accent-ink"?: string;
|
|
62
|
+
/** Hover/active shade of `primary`. Default: a derived darken of `primary`. */
|
|
63
|
+
"primary-hover"?: string;
|
|
64
|
+
/** Hover/active shade of `accent`. Default: a derived darken of `accent`. */
|
|
65
|
+
"accent-hover"?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ThemeFontFamily {
|
|
69
|
+
family: string;
|
|
70
|
+
weights: number[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ThemeTypeTokens {
|
|
74
|
+
display: ThemeFontFamily;
|
|
75
|
+
body: ThemeFontFamily;
|
|
76
|
+
mono?: ThemeFontFamily;
|
|
77
|
+
/** Type scale ratio, e.g. 1.25 (major third). */
|
|
78
|
+
scale: number;
|
|
79
|
+
tracking?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ThemeSpaceTokens {
|
|
83
|
+
/** Base spacing unit in px. */
|
|
84
|
+
base: number;
|
|
85
|
+
/** Multiplier scale, e.g. [0, 0.25, 0.5, 1, 1.5, 2, 3, 4, 6, 8]. */
|
|
86
|
+
scale: number[];
|
|
87
|
+
containerMaxWidth: string;
|
|
88
|
+
sectionRhythm: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ThemeShapeTokens {
|
|
92
|
+
radius: { sm: string; md: string; lg: string; full: string };
|
|
93
|
+
borderWeights: { thin: string; medium: string; thick: string };
|
|
94
|
+
shadow: { sm: string; md: string; lg: string };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ThemeBrandTokens {
|
|
98
|
+
logoLight: string;
|
|
99
|
+
logoDark: string;
|
|
100
|
+
favicon: string;
|
|
101
|
+
ogImage: string;
|
|
102
|
+
/** "full" | "reduced" — tenant-level default motion preference. */
|
|
103
|
+
motion: "full" | "reduced";
|
|
104
|
+
/**
|
|
105
|
+
* Header treatment. "dark" renders the bar in the primary color carrying the
|
|
106
|
+
* light logo (matches brands whose header is dark + white logo, e.g. vape
|
|
107
|
+
* stores); "light" (default) is a light bar with the dark logo/wordmark.
|
|
108
|
+
*/
|
|
109
|
+
headerVariant?: "light" | "dark";
|
|
110
|
+
/**
|
|
111
|
+
* Hero treatment. "bold" (default) paints the hero full-bleed in the primary
|
|
112
|
+
* color (one signature moment). "light" renders the hero on the page
|
|
113
|
+
* background with the brand color used only as an accent on the headline/CTA —
|
|
114
|
+
* matches merchants whose own site is a light page with color accents. The
|
|
115
|
+
* brand adopter sets "light" when it captures a real hero image.
|
|
116
|
+
*/
|
|
117
|
+
heroVariant?: "bold" | "light";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface ThemeTokens {
|
|
121
|
+
color: ThemeColorTokens;
|
|
122
|
+
type: ThemeTypeTokens;
|
|
123
|
+
space: ThemeSpaceTokens;
|
|
124
|
+
shape: ThemeShapeTokens;
|
|
125
|
+
brand: ThemeBrandTokens;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* California Prop 65 warning content. The safe-harbor warning FORMAT is mandated
|
|
130
|
+
* (Cal. Code Regs. tit. 27 § 25601+): the component owns the fixed skeleton
|
|
131
|
+
* ("WARNING: … known to the State of California to cause …") and the official
|
|
132
|
+
* warnings URL; only the variable slots — which harm endpoint(s) the warning
|
|
133
|
+
* cites and the chemical name(s) named in the long form — come from tenant data.
|
|
134
|
+
* Never author the sentence in tenant/marketing copy; drive it from here.
|
|
135
|
+
*/
|
|
136
|
+
export interface Prop65Config {
|
|
137
|
+
/** Harm endpoint(s) the mandated clause cites. */
|
|
138
|
+
harm: "cancer" | "reproductive" | "cancer-and-reproductive";
|
|
139
|
+
/**
|
|
140
|
+
* Chemical name(s) named in the LONG-form warning (e.g. ["nicotine"]). When
|
|
141
|
+
* present the component renders the long form ("…including {chemicals}, which
|
|
142
|
+
* is/are known…"); omit for the short-form warning.
|
|
143
|
+
*/
|
|
144
|
+
chemicals?: string[];
|
|
145
|
+
/** Official warnings URL. Defaults to www.P65Warnings.ca.gov. */
|
|
146
|
+
url?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Purchase-quantity-limit DISPLAY. Display surface only — the limit is surfaced
|
|
151
|
+
* for the shopper; enforcement stays in the product/commerce layer, never hand-
|
|
152
|
+
* built here (per decision compliance-widgets-platform-owned).
|
|
153
|
+
*/
|
|
154
|
+
export interface PurchaseLimitConfig {
|
|
155
|
+
/** Human-readable limit, e.g. "2 units per order". Display only. */
|
|
156
|
+
text: string;
|
|
157
|
+
/** Optional short reason/citation, e.g. "per state law". */
|
|
158
|
+
reason?: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* State/jurisdiction sale-eligibility DISPLAY. Display surface only — the widget
|
|
163
|
+
* SHOWS the tenant-configured eligibility messaging; it does NOT compute whether
|
|
164
|
+
* a given cart/address is eligible (that stays in the product/commerce layer,
|
|
165
|
+
* per decision compliance-widgets-platform-owned). List the restricted OR the
|
|
166
|
+
* eligible jurisdictions (or supply a custom note); the widget composes a
|
|
167
|
+
* default sentence when `note` is omitted.
|
|
168
|
+
*/
|
|
169
|
+
export interface StateEligibilityConfig {
|
|
170
|
+
/** State/jurisdiction codes or names where sale/shipment is NOT available. */
|
|
171
|
+
restrictedStates?: string[];
|
|
172
|
+
/** Allowlist — sale/shipment available ONLY in these. */
|
|
173
|
+
eligibleStates?: string[];
|
|
174
|
+
/** Custom display message; when omitted a sentence is composed from the lists. */
|
|
175
|
+
note?: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Excise-tax DISPLAY cue. Display surface only — it surfaces that applicable
|
|
180
|
+
* excise taxes apply and (optionally) a per-jurisdiction breakdown for shopper
|
|
181
|
+
* transparency; the actual tax CALCULATION is product/commerce-owned (never
|
|
182
|
+
* computed here), per decision compliance-widgets-platform-owned.
|
|
183
|
+
*/
|
|
184
|
+
export interface ExciseTaxConfig {
|
|
185
|
+
/** Display note, e.g. "State vapor excise taxes are calculated at checkout." */
|
|
186
|
+
note: string;
|
|
187
|
+
/** Optional per-jurisdiction lines (display only), e.g. { label: "CA", detail: "$0.05/ml" }. */
|
|
188
|
+
jurisdictions?: Array<{ label: string; detail: string }>;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Regulated-commerce compliance (e.g. vape/nicotine). Browse-surface only —
|
|
193
|
+
* the actual identity/age VERIFICATION is Token of Trust's product and is wired
|
|
194
|
+
* in a later phase; this config drives storefront affordances (FDA warning, the
|
|
195
|
+
* soft 21+ affirmation gate, shipping-restriction / jurisdiction notices).
|
|
196
|
+
* Omit/undefined for unregulated tenants and none of it renders.
|
|
197
|
+
*
|
|
198
|
+
* Every field below is data-driven: the matching platform widget in
|
|
199
|
+
* components/compliance/ renders ONLY when its field is present and NEVER
|
|
200
|
+
* fabricates or hand-authors mandated legal copy (per decision
|
|
201
|
+
* compliance-widgets-platform-owned). Additive — a tenant lists only what applies.
|
|
202
|
+
*/
|
|
203
|
+
export interface ComplianceConfig {
|
|
204
|
+
/** Minimum age to enter. Drives the soft affirmation gate (e.g. 21). */
|
|
205
|
+
minAge?: number;
|
|
206
|
+
/** Render the FDA nicotine warning sitewide + reinforced on PDP. */
|
|
207
|
+
nicotineWarning?: boolean;
|
|
208
|
+
/** Short PACT-Act / no-ship-states / adult-signature notice (plain text). */
|
|
209
|
+
shippingRestriction?: string;
|
|
210
|
+
/**
|
|
211
|
+
* Render the PACT Act compliance notice (ENDS/vapor shipments): age
|
|
212
|
+
* verification (21+), adult signature on delivery, no USPS carriage. The
|
|
213
|
+
* federal-requirement wording is fixed in PactActNotice; this is the on/off.
|
|
214
|
+
*/
|
|
215
|
+
pactAct?: boolean;
|
|
216
|
+
/**
|
|
217
|
+
* Render the adult-signature-on-delivery notice. Wording is fixed in
|
|
218
|
+
* AdultSignatureNotice; the age reflects `minAge` (defaults to 21).
|
|
219
|
+
*/
|
|
220
|
+
adultSignature?: boolean;
|
|
221
|
+
/**
|
|
222
|
+
* California Prop 65 warning. `true` renders the short cancer+reproductive
|
|
223
|
+
* safe-harbor warning; a Prop65Config selects the harm endpoint(s), chemical
|
|
224
|
+
* name(s), and URL. Absent → nothing renders.
|
|
225
|
+
*/
|
|
226
|
+
prop65?: Prop65Config | boolean;
|
|
227
|
+
/** Purchase-quantity-limit display (display only; enforcement is product-owned). */
|
|
228
|
+
purchaseLimit?: PurchaseLimitConfig;
|
|
229
|
+
/**
|
|
230
|
+
* State/jurisdiction sale-eligibility display (display only; the eligibility
|
|
231
|
+
* DECISION for a given cart/address stays product-owned).
|
|
232
|
+
*/
|
|
233
|
+
stateEligibility?: StateEligibilityConfig;
|
|
234
|
+
/**
|
|
235
|
+
* Excise-tax display cue (display only; the tax CALCULATION stays
|
|
236
|
+
* product/commerce-owned).
|
|
237
|
+
*/
|
|
238
|
+
exciseTax?: ExciseTaxConfig;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* What a tenant's site is FOR. "commerce" (default) is the browse/discover
|
|
243
|
+
* ecommerce storefront the platform was born as. "marketing" is a non-commerce
|
|
244
|
+
* B2B/SaaS site — chrome, rich page composition, and conversion CTAs, with the
|
|
245
|
+
* catalog/cart affordances switched off. The mode picks feature DEFAULTS
|
|
246
|
+
* (see resolveTenantFeatures); `features` overrides individual flags.
|
|
247
|
+
*/
|
|
248
|
+
export type SiteType = "commerce" | "marketing";
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Per-tenant capability switches. These gate whole surfaces so the same
|
|
252
|
+
* component library renders both a store and a marketing site without any
|
|
253
|
+
* tenant-specific `if` in shared code. Every flag has a sensible default keyed
|
|
254
|
+
* off `siteType`; a tenant only lists the ones it wants to flip.
|
|
255
|
+
*/
|
|
256
|
+
export interface TenantFeatures {
|
|
257
|
+
/** Catalog surfaces: /collections, /products/*, listing grids, PDPs. */
|
|
258
|
+
catalog: boolean;
|
|
259
|
+
/** Header search typeahead island. */
|
|
260
|
+
productSearch: boolean;
|
|
261
|
+
/** Wishlist / "saved items" surface + header heart control. */
|
|
262
|
+
savedItems: boolean;
|
|
263
|
+
/** Product quick-view dialog island. */
|
|
264
|
+
quickView: boolean;
|
|
265
|
+
/** Newsletter capture forms (footer + homepage block). */
|
|
266
|
+
newsletter: boolean;
|
|
267
|
+
/** Product/collection URLs in the XML sitemap. */
|
|
268
|
+
commerceSitemap: boolean;
|
|
269
|
+
/** Product-offer JSON-LD on product/collection pages. */
|
|
270
|
+
productJsonLd: boolean;
|
|
271
|
+
/** Soft age-affirmation gate + regulated-commerce chrome (also needs `compliance`). */
|
|
272
|
+
ageGate: boolean;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const COMMERCE_FEATURES: TenantFeatures = {
|
|
276
|
+
catalog: true,
|
|
277
|
+
productSearch: true,
|
|
278
|
+
savedItems: true,
|
|
279
|
+
quickView: true,
|
|
280
|
+
newsletter: true,
|
|
281
|
+
commerceSitemap: true,
|
|
282
|
+
productJsonLd: true,
|
|
283
|
+
ageGate: true,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const MARKETING_FEATURES: TenantFeatures = {
|
|
287
|
+
catalog: false,
|
|
288
|
+
productSearch: false,
|
|
289
|
+
savedItems: false,
|
|
290
|
+
quickView: false,
|
|
291
|
+
newsletter: false,
|
|
292
|
+
commerceSitemap: false,
|
|
293
|
+
productJsonLd: false,
|
|
294
|
+
ageGate: false,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/** Feature defaults for a site type (returns a fresh copy — safe to mutate). */
|
|
298
|
+
export function siteTypeDefaults(siteType: SiteType): TenantFeatures {
|
|
299
|
+
return { ...(siteType === "marketing" ? MARKETING_FEATURES : COMMERCE_FEATURES) };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Resolve a tenant's effective features: siteType defaults, with any explicit
|
|
304
|
+
* `features` overrides layered on top (undefined entries ignored, so a tenant
|
|
305
|
+
* can flip one flag without restating the rest). Unset `siteType` == "commerce",
|
|
306
|
+
* preserving the platform's original behavior for existing tenants.
|
|
307
|
+
*/
|
|
308
|
+
export function resolveTenantFeatures(
|
|
309
|
+
tenant: Pick<TenantConfig, "siteType" | "features">,
|
|
310
|
+
): TenantFeatures {
|
|
311
|
+
const base = siteTypeDefaults(tenant.siteType ?? "commerce");
|
|
312
|
+
if (!tenant.features) return base;
|
|
313
|
+
for (const [k, v] of Object.entries(tenant.features)) {
|
|
314
|
+
if (v !== undefined) base[k as keyof TenantFeatures] = v;
|
|
315
|
+
}
|
|
316
|
+
return base;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export interface TenantConfig {
|
|
320
|
+
tenant_id: string;
|
|
321
|
+
/** ToT-side merchant identifier (appDomain), e.g. "pilot.tokenoftrust.com". */
|
|
322
|
+
appDomain: string;
|
|
323
|
+
/** The hostname(s) that resolve to this tenant. */
|
|
324
|
+
domains: string[];
|
|
325
|
+
/** Primary canonical domain for SEO/canonical URLs. */
|
|
326
|
+
canonicalDomain: string;
|
|
327
|
+
displayName: string;
|
|
328
|
+
/** Storyblok delivery token for this tenant's child space. */
|
|
329
|
+
storyblok_space_token: string;
|
|
330
|
+
/** Data scope passed to the ToT catalog API (usually == appDomain). */
|
|
331
|
+
tot_data_scope: string;
|
|
332
|
+
/** Per-tenant theme overrides; merged over the reference theme defaults. */
|
|
333
|
+
theme_tokens: DeepPartial<ThemeTokens>;
|
|
334
|
+
/** Currency for display formatting. */
|
|
335
|
+
currency: string;
|
|
336
|
+
locale: string;
|
|
337
|
+
/**
|
|
338
|
+
* What the site is for: "commerce" (default) or "marketing". Drives feature
|
|
339
|
+
* defaults (resolveTenantFeatures) and non-commerce chrome. Omitted == commerce.
|
|
340
|
+
*/
|
|
341
|
+
siteType?: SiteType;
|
|
342
|
+
/** Explicit feature overrides layered atop the siteType defaults. */
|
|
343
|
+
features?: Partial<TenantFeatures>;
|
|
344
|
+
/** Regulated-commerce affordances; undefined for unregulated tenants. */
|
|
345
|
+
compliance?: ComplianceConfig;
|
|
346
|
+
/**
|
|
347
|
+
* Cart/checkout engine wiring (FoxyCart, Track A). Present on commerce tenants
|
|
348
|
+
* that have a FoxyCart store; drives the loader + signed add-to-cart. The
|
|
349
|
+
* `storeSubDomain` here is a default — middleware may override it from env
|
|
350
|
+
* (FOXY_STORE_SUBDOMAIN) so the demo store swaps with zero code change. The
|
|
351
|
+
* signing SECRET is never stored on the tenant; it's an env Worker binding.
|
|
352
|
+
*/
|
|
353
|
+
commerce?: {
|
|
354
|
+
foxy?: FoxyCommerceConfig;
|
|
355
|
+
};
|
|
356
|
+
/**
|
|
357
|
+
* PR-gated capability switches (cart/checkout, age/identity verification,
|
|
358
|
+
* excise tax). Source of truth is the tenant repo's `.tot/config.json`; this
|
|
359
|
+
* build-time default is overridden per environment by the published
|
|
360
|
+
* capabilities artifact (localhost=working tree, preview=PR head, live=promoted).
|
|
361
|
+
* Undefined entries default from siteType/compliance — see resolveCapabilities.
|
|
362
|
+
*/
|
|
363
|
+
capabilities?: Partial<CapabilityConfig>;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export type DeepPartial<T> = {
|
|
367
|
+
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
/** The interface the tenant-resolution middleware implements. */
|
|
371
|
+
export interface TenantResolver {
|
|
372
|
+
/**
|
|
373
|
+
* Resolve a tenant for a hostname, falling back to a default tenant for
|
|
374
|
+
* unknown hosts (Phase 1 convenience so preview/local hosts still render).
|
|
375
|
+
*/
|
|
376
|
+
resolveByHost(host: string): Promise<TenantConfig | null>;
|
|
377
|
+
/**
|
|
378
|
+
* Strict match: return the tenant that explicitly claims `host` in its
|
|
379
|
+
* `domains`, or null. Unlike resolveByHost it never falls back to a default,
|
|
380
|
+
* so callers can detect "this host/segment owns no tenant" — used by the
|
|
381
|
+
* `/<domain>/*` path-prefix routing fallback.
|
|
382
|
+
*/
|
|
383
|
+
resolveStrict(host: string): Promise<TenantConfig | null>;
|
|
384
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Prebuild step for the colocated tenant layout.
|
|
4
|
+
*
|
|
5
|
+
* A tenant IS the repo-root directory `tenants/<id>/` (holding content, public,
|
|
6
|
+
* theme.json). Astro serves static assets from `apps/storefront/public/`, so a
|
|
7
|
+
* tenant's public files must land at `apps/storefront/public/tenants/<id>/`
|
|
8
|
+
* for the `/tenants/<id>/...` URLs to resolve. This script mirrors every tenant's
|
|
9
|
+
* public directory into that generated location.
|
|
10
|
+
*
|
|
11
|
+
* The destination (`apps/storefront/public/tenants/`) is a BUILD ARTIFACT
|
|
12
|
+
* (gitignored) — the source of truth is `tenants/<id>/public/`. Wired into the
|
|
13
|
+
* storefront `prebuild`/`predev` lifecycle (package.json) and invoked directly by
|
|
14
|
+
* `scripts/tot-dev.mjs` before it boots `astro dev`.
|
|
15
|
+
*
|
|
16
|
+
* Idempotent: it wipes+rewrites the destination on every run. `TENANT_ASSETS_DEST`
|
|
17
|
+
* overrides the destination (used by tests / spikes).
|
|
18
|
+
*
|
|
19
|
+
* VERSIONED TENANTS (decision `assets-r2-versioned`): a tenant whose assets are
|
|
20
|
+
* served version-scoped from R2 (src/pages/tenants/[id]/[...path].ts) must NOT be
|
|
21
|
+
* baked here — otherwise the static file would shadow the R2 route. Opt a tenant
|
|
22
|
+
* out by adding a `tenants/<id>/.tot/assets-versioned` marker file, or by listing
|
|
23
|
+
* its id in the `TENANT_ASSETS_VERSIONED` env (comma-separated). Non-versioned
|
|
24
|
+
* tenants keep the baked path unchanged (back-compat).
|
|
25
|
+
*/
|
|
26
|
+
import { cp, mkdir, readdir, rm } from "node:fs/promises";
|
|
27
|
+
import { existsSync } from "node:fs";
|
|
28
|
+
import { join, resolve, dirname } from "node:path";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
|
|
31
|
+
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
32
|
+
const src = resolve(repoRoot, "tenants");
|
|
33
|
+
const dest = process.env.TENANT_ASSETS_DEST
|
|
34
|
+
? resolve(process.env.TENANT_ASSETS_DEST)
|
|
35
|
+
: resolve(repoRoot, "apps/storefront/public/tenants");
|
|
36
|
+
|
|
37
|
+
if (!existsSync(src)) process.exit(0);
|
|
38
|
+
|
|
39
|
+
await rm(dest, { recursive: true, force: true });
|
|
40
|
+
|
|
41
|
+
const versionedEnv = new Set(
|
|
42
|
+
(process.env.TENANT_ASSETS_VERSIONED ?? "")
|
|
43
|
+
.split(",")
|
|
44
|
+
.map((s) => s.trim())
|
|
45
|
+
.filter(Boolean),
|
|
46
|
+
);
|
|
47
|
+
const isVersioned = (id) =>
|
|
48
|
+
versionedEnv.has(id) ||
|
|
49
|
+
existsSync(join(src, id, ".tot", "assets-versioned"));
|
|
50
|
+
|
|
51
|
+
let copied = 0;
|
|
52
|
+
let skipped = 0;
|
|
53
|
+
for (const id of await readdir(src)) {
|
|
54
|
+
const pub = join(src, id, "public");
|
|
55
|
+
if (!existsSync(pub)) continue;
|
|
56
|
+
// Versioned tenants are served from R2 — baking would shadow the R2 route.
|
|
57
|
+
if (isVersioned(id)) {
|
|
58
|
+
skipped++;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
await mkdir(join(dest, id), { recursive: true });
|
|
62
|
+
await cp(pub, join(dest, id), { recursive: true });
|
|
63
|
+
copied++;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const skipNote = skipped ? ` (skipped ${skipped} versioned → R2)` : "";
|
|
67
|
+
console.log(
|
|
68
|
+
`copy-tenant-assets: mirrored ${copied} tenant public/ tree(s) → ${dest}${skipNote}`,
|
|
69
|
+
);
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The inline-AI edit loop's dev-server-side handlers (WS5 G1): the floating
|
|
3
|
+
* "✦ ask" box (see ai-widget-client.js) posts a prompt here; this resolves the
|
|
4
|
+
* developer's MCP session, calls the `content_edit` tool with the tenant's
|
|
5
|
+
* current content/theme, applies the returned unified diffs to disk (which
|
|
6
|
+
* fires the existing tenantHotReload/Vite HMR — see astro.config.mjs), and
|
|
7
|
+
* records the edit for one-key undo. Wired into `astro dev` as a Vite
|
|
8
|
+
* middleware plugin (astro.config.mjs's `inlineAiEditPlugin`), so it only
|
|
9
|
+
* ever runs in `astro dev` — never bundled into the production Cloudflare
|
|
10
|
+
* Worker (see the plugin's `apply: "serve"`).
|
|
11
|
+
*
|
|
12
|
+
* Reuses the CLI's own MCP transport + developer-session resolution
|
|
13
|
+
* (packages/cli/src/mcp.mjs, auth.mjs) via a plain relative import — both are
|
|
14
|
+
* dependency-free and the astro dev process runs on the same host/user as the
|
|
15
|
+
* `tot` CLI, so `~/.tot/credentials.json` is directly readable. No token
|
|
16
|
+
* plumbing needed between the CLI and the dev server.
|
|
17
|
+
*
|
|
18
|
+
* Scope (decision `d2-resolved-inline-ai-scope`): content + theme.json ONLY.
|
|
19
|
+
* The MCP tool enforces this server-side; nothing here widens it.
|
|
20
|
+
*/
|
|
21
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from "node:fs";
|
|
22
|
+
import { randomUUID } from "node:crypto";
|
|
23
|
+
import { join, dirname } from "node:path";
|
|
24
|
+
import { createMcpClient } from "../../packages/cli/src/mcp.mjs";
|
|
25
|
+
import { resolveDeveloperSession, AuthUnavailableError } from "../../packages/cli/src/auth.mjs";
|
|
26
|
+
import { applyUnifiedDiff } from "./unified-diff.mjs";
|
|
27
|
+
|
|
28
|
+
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
29
|
+
|
|
30
|
+
/** Where a tenant's editable files + AI-edit history live (monorepo layout). */
|
|
31
|
+
export function tenantPaths(repoRoot, tenantId) {
|
|
32
|
+
const dir = join(repoRoot, "tenants", tenantId);
|
|
33
|
+
return {
|
|
34
|
+
dir,
|
|
35
|
+
home: join(dir, "content", "home.html"),
|
|
36
|
+
theme: join(dir, "theme.json"),
|
|
37
|
+
historyFile: join(dir, ".tot", "ai-history.jsonl"),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readFileIfPresent(path) {
|
|
42
|
+
return existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function appendHistory(historyFile, entry) {
|
|
46
|
+
mkdirSync(dirname(historyFile), { recursive: true });
|
|
47
|
+
appendFileSync(historyFile, `${JSON.stringify(entry)}\n`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readHistory(historyFile) {
|
|
51
|
+
if (!existsSync(historyFile)) return [];
|
|
52
|
+
return readFileSync(historyFile, "utf8")
|
|
53
|
+
.split("\n")
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.map((line) => JSON.parse(line));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve an authenticated MCP client for the developer signed in to THIS
|
|
60
|
+
* machine (`tot login`). Never refreshes an expired token — mirrors the
|
|
61
|
+
* "silently refresh" case as an explicit ask to re-run `tot login`, matching
|
|
62
|
+
* the CLI's own AuthUnavailableError guidance rather than an obscure 500.
|
|
63
|
+
*/
|
|
64
|
+
async function resolveClient(mcpBaseUrl) {
|
|
65
|
+
const client = createMcpClient(mcpBaseUrl || process.env.TOT_MCP_URL || DEFAULT_MCP_URL);
|
|
66
|
+
await client.initialize();
|
|
67
|
+
try {
|
|
68
|
+
await resolveDeveloperSession(client, process.env);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
if (e instanceof AuthUnavailableError) {
|
|
71
|
+
throw new Error(`${e.message} ${e.hint || ""}`.trim());
|
|
72
|
+
}
|
|
73
|
+
throw e;
|
|
74
|
+
}
|
|
75
|
+
return client;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Handle a "✦ ask" submission: prompt + tenant → MCP content_edit → diffs
|
|
80
|
+
* applied to disk + one history entry. Returns a plain object the widget
|
|
81
|
+
* renders directly (never throws for expected failure modes — auth/refusal/
|
|
82
|
+
* not-configured all come back as `{ ok:false, status, message }`).
|
|
83
|
+
*/
|
|
84
|
+
export async function handleAiEdit({ repoRoot, tenantId, prompt, mcpBaseUrl }, deps = {}) {
|
|
85
|
+
const { resolveClientImpl = resolveClient } = deps;
|
|
86
|
+
const paths = tenantPaths(repoRoot, tenantId);
|
|
87
|
+
const files = [
|
|
88
|
+
{ path: "content/home.html", content: readFileIfPresent(paths.home) },
|
|
89
|
+
{ path: "theme.json", content: readFileIfPresent(paths.theme) },
|
|
90
|
+
].filter((f) => f.content !== null);
|
|
91
|
+
|
|
92
|
+
if (files.length === 0) {
|
|
93
|
+
return { ok: false, status: "no_files", message: `No content/home.html or theme.json found under tenants/${tenantId}/.` };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let client;
|
|
97
|
+
try {
|
|
98
|
+
client = await resolveClientImpl(mcpBaseUrl);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
return { ok: false, status: "unauthenticated", message: e.message };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let result;
|
|
104
|
+
try {
|
|
105
|
+
result = await client.callTool("content_edit", { prompt, files });
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return { ok: false, status: "error", message: e.message };
|
|
108
|
+
}
|
|
109
|
+
if (result?.status && result.status !== "ok") {
|
|
110
|
+
return { ok: false, status: result.status, message: result.message || "content_edit refused the request." };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const diffs = Array.isArray(result?.diffs) ? result.diffs : [];
|
|
114
|
+
if (diffs.length === 0) {
|
|
115
|
+
return { ok: true, status: "noop", summary: result?.summary || "No change was needed.", diffs: [] };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const pathFor = (relPath) => (relPath === "content/home.html" ? paths.home : relPath === "theme.json" ? paths.theme : null);
|
|
119
|
+
const applied = [];
|
|
120
|
+
for (const d of diffs) {
|
|
121
|
+
const target = pathFor(d.path);
|
|
122
|
+
if (!target) {
|
|
123
|
+
return { ok: false, status: "out_of_scope", message: `content_edit returned a diff for an unexpected path: ${d.path}` };
|
|
124
|
+
}
|
|
125
|
+
const before = readFileIfPresent(target) ?? "";
|
|
126
|
+
let after;
|
|
127
|
+
try {
|
|
128
|
+
after = applyUnifiedDiff(before, d.diff);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
return { ok: false, status: "diff_apply_failed", message: `Could not apply the diff for ${d.path}: ${e.message}` };
|
|
131
|
+
}
|
|
132
|
+
applied.push({ path: d.path, target, before, after, diff: d.diff });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Write only after every diff in the batch applies cleanly — a partial
|
|
136
|
+
// apply would leave content/theme inconsistent with each other.
|
|
137
|
+
for (const a of applied) writeFileSync(a.target, a.after);
|
|
138
|
+
|
|
139
|
+
const historyId = randomUUID();
|
|
140
|
+
appendHistory(paths.historyFile, {
|
|
141
|
+
id: historyId,
|
|
142
|
+
at: new Date().toISOString(),
|
|
143
|
+
kind: "edit",
|
|
144
|
+
prompt,
|
|
145
|
+
summary: result.summary,
|
|
146
|
+
files: applied.map(({ path, before, after, diff }) => ({ path, before, after, diff })),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
ok: true,
|
|
151
|
+
status: "ok",
|
|
152
|
+
summary: result.summary,
|
|
153
|
+
diffs: applied.map(({ path, diff }) => ({ path, diff })),
|
|
154
|
+
historyId,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* One-key undo: restore the most recent (non-undone) edit's files to their
|
|
160
|
+
* pre-edit content, and record the undo as its own history entry so the log
|
|
161
|
+
* stays an honest append-only account of what happened.
|
|
162
|
+
*/
|
|
163
|
+
export function handleAiUndo({ repoRoot, tenantId }) {
|
|
164
|
+
const paths = tenantPaths(repoRoot, tenantId);
|
|
165
|
+
const history = readHistory(paths.historyFile);
|
|
166
|
+
const undoneIds = new Set(history.filter((h) => h.kind === "undo").map((h) => h.undoes));
|
|
167
|
+
const lastEdit = [...history].reverse().find((h) => h.kind === "edit" && !undoneIds.has(h.id));
|
|
168
|
+
|
|
169
|
+
if (!lastEdit) {
|
|
170
|
+
return { ok: false, status: "nothing_to_undo", message: "No AI edit to undo." };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const f of lastEdit.files) writeFileSync(f.path === "content/home.html" ? paths.home : paths.theme, f.before);
|
|
174
|
+
|
|
175
|
+
appendHistory(paths.historyFile, {
|
|
176
|
+
id: randomUUID(),
|
|
177
|
+
at: new Date().toISOString(),
|
|
178
|
+
kind: "undo",
|
|
179
|
+
undoes: lastEdit.id,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return { ok: true, status: "ok", restoredPaths: lastEdit.files.map((f) => f.path), undid: lastEdit.summary };
|
|
183
|
+
}
|