@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,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive the /dev "store status" panel — a small, honest, per-tenant readout of what's
|
|
3
|
+
* already wired up for the signed-in developer's own store, computed from the resolved
|
|
4
|
+
* TenantConfig (+ whether the viewer has developer access). Pure and renderer-safe: it
|
|
5
|
+
* reads only public tenant shape, returns display strings, and NEVER emits a secret.
|
|
6
|
+
*
|
|
7
|
+
* SECRETS: the Storyblok delivery token is checked ONLY for presence (real vs the
|
|
8
|
+
* "…PLACEHOLDER" default) to decide the editorial row — the token value is never put in
|
|
9
|
+
* the returned rows. No session id, email, capability value, or magic-link data is read.
|
|
10
|
+
*
|
|
11
|
+
* PHASE HONESTY: cart/checkout/payments/excise/age-verification are Phase 2+. Even when a
|
|
12
|
+
* tenant's build-time `capabilities` flags are on, this platform hasn't shipped those, so
|
|
13
|
+
* they are ALWAYS reported as a single "coming later" row — never as live. The panel only
|
|
14
|
+
* marks something ✓ when Phase 1 actually serves it (route, catalog, theme, editorial,
|
|
15
|
+
* search, and the compliance guardrails that gate a submit).
|
|
16
|
+
*/
|
|
17
|
+
import type { TenantConfig } from "@tot/public-runtime";
|
|
18
|
+
|
|
19
|
+
export type DevStatusState = "ok" | "todo" | "future";
|
|
20
|
+
|
|
21
|
+
export interface DevStatusRow {
|
|
22
|
+
/** Stable key (for test assertions / keys). */
|
|
23
|
+
key: string;
|
|
24
|
+
/** Human label — the thing being reported. */
|
|
25
|
+
label: string;
|
|
26
|
+
/** Short value — the current standing. */
|
|
27
|
+
value: string;
|
|
28
|
+
state: DevStatusState;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DevStoreStatus {
|
|
32
|
+
/** The tenant's display name, or null when no tenant resolved. */
|
|
33
|
+
storeName: string | null;
|
|
34
|
+
appDomain: string | null;
|
|
35
|
+
/** Best hosted URL to "open your storefront", or null. */
|
|
36
|
+
liveUrl: string | null;
|
|
37
|
+
/** True when this store carries regulated-commerce guardrails. */
|
|
38
|
+
regulated: boolean;
|
|
39
|
+
rows: DevStatusRow[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A Storyblok token counts as configured only when it isn't the build placeholder. */
|
|
43
|
+
export function editorialConfigured(token: string | undefined | null): boolean {
|
|
44
|
+
return !!token && !/PLACEHOLDER$/i.test(token) && token.trim().length > 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Marketing sites have no catalog/search; everything else (commerce) does. */
|
|
48
|
+
function isCommerce(tenant: TenantConfig): boolean {
|
|
49
|
+
return tenant.siteType !== "marketing";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A tenant has a customized theme when it ships any theme_tokens overrides. */
|
|
53
|
+
function hasCustomTheme(tenant: TenantConfig): boolean {
|
|
54
|
+
return !!tenant.theme_tokens && Object.keys(tenant.theme_tokens).length > 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Best hosted URL for "Open your storefront": prefer the tenant's own
|
|
59
|
+
* `*.tokenoftrust.store` hosted-store subdomain (proven real for pilot tenants); else
|
|
60
|
+
* fall back to the shared storefront host at the tenant's path.
|
|
61
|
+
*/
|
|
62
|
+
export function hostedStoreUrl(tenant: TenantConfig | null): string | null {
|
|
63
|
+
if (!tenant) return null;
|
|
64
|
+
const sub = tenant.domains.find((d) => d.toLowerCase().endsWith(".tokenoftrust.store"));
|
|
65
|
+
if (sub) return `https://${sub}/`;
|
|
66
|
+
return `https://storefront.tokenoftrust.store/${tenant.appDomain}/`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build the status readout for `tenant`. Rows are ordered "what's wired → what's a
|
|
71
|
+
* guardrail → what's coming later" so the developer reads a story, not a dashboard.
|
|
72
|
+
*/
|
|
73
|
+
export function deriveStoreStatus(tenant: TenantConfig | null): DevStoreStatus {
|
|
74
|
+
if (!tenant) {
|
|
75
|
+
return {
|
|
76
|
+
storeName: null,
|
|
77
|
+
appDomain: null,
|
|
78
|
+
liveUrl: null,
|
|
79
|
+
regulated: false,
|
|
80
|
+
rows: [
|
|
81
|
+
{ key: "route", label: "Storefront route", value: "ready", state: "ok" },
|
|
82
|
+
{ key: "store", label: "Your store", value: "sign in to detect", state: "todo" },
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const commerce = isCommerce(tenant);
|
|
88
|
+
const editorial = editorialConfigured(tenant.storyblok_space_token);
|
|
89
|
+
const regulated = !!tenant.compliance;
|
|
90
|
+
const rows: DevStatusRow[] = [];
|
|
91
|
+
|
|
92
|
+
rows.push({ key: "route", label: "Storefront route", value: "live", state: "ok" });
|
|
93
|
+
rows.push({ key: "store", label: "Your store", value: tenant.displayName, state: "ok" });
|
|
94
|
+
|
|
95
|
+
rows.push(
|
|
96
|
+
commerce
|
|
97
|
+
? { key: "catalog", label: "Catalog", value: "connected", state: "ok" }
|
|
98
|
+
: { key: "catalog", label: "Catalog", value: "marketing site — none", state: "future" },
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
rows.push({
|
|
102
|
+
key: "theme",
|
|
103
|
+
label: "Theme",
|
|
104
|
+
value: hasCustomTheme(tenant) ? "your theme detected" : "reference theme",
|
|
105
|
+
state: "ok",
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
rows.push(
|
|
109
|
+
editorial
|
|
110
|
+
? { key: "editorial", label: "Editorial (Storyblok)", value: "connected", state: "ok" }
|
|
111
|
+
: { key: "editorial", label: "Editorial", value: "local content", state: "ok" },
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (commerce) {
|
|
115
|
+
rows.push({ key: "search", label: "Search", value: "enabled", state: "ok" });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (regulated) {
|
|
119
|
+
const minAge = tenant.compliance?.minAge;
|
|
120
|
+
rows.push({
|
|
121
|
+
key: "guardrails",
|
|
122
|
+
label: "Compliance guardrails",
|
|
123
|
+
value: minAge ? `${minAge}+ · checked before ship` : "checked before ship",
|
|
124
|
+
state: "ok",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
rows.push({
|
|
129
|
+
key: "phase2",
|
|
130
|
+
label: "Cart · checkout · payments · age check",
|
|
131
|
+
value: "Phase 2+",
|
|
132
|
+
state: "future",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
storeName: tenant.displayName,
|
|
137
|
+
appDomain: tenant.appDomain,
|
|
138
|
+
liveUrl: hostedStoreUrl(tenant),
|
|
139
|
+
regulated,
|
|
140
|
+
rows,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction helpers for diagnostic logs — keep traces correlatable without persisting PII.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Mask an email for logs: keep the first char + the domain (`d***@example.com`). */
|
|
6
|
+
export function maskEmail(email: string): string {
|
|
7
|
+
const at = email.indexOf("@");
|
|
8
|
+
if (at <= 0) return "***";
|
|
9
|
+
return `${email[0]}***${email.slice(at)}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Email for a diagnostic line: the FULL address in local dev only (`import.meta.env.DEV`
|
|
14
|
+
* is a static boolean → this branch is tree-shaken from the production worker), where logs
|
|
15
|
+
* are local + gitignored and the raw address is needed for debugging; MASKED everywhere the
|
|
16
|
+
* build is deployed, so prod/worker logs never persist raw recipient PII.
|
|
17
|
+
*/
|
|
18
|
+
export function diagEmail(email: string): string {
|
|
19
|
+
return import.meta.env.DEV ? email : maskEmail(email);
|
|
20
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full-page edge cache for production browse reads (Epic Phase-2 D1).
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 cached only the catalog *API* calls (HttpToTClient); the rendered HTML
|
|
5
|
+
* was re-built on every request even though browse pages change rarely. This
|
|
6
|
+
* caches the final HTML response in the Workers Cache API and short-circuits the
|
|
7
|
+
* whole render on a hit.
|
|
8
|
+
*
|
|
9
|
+
* Invalidation is a per-tenant **cache version** integer in TENANT_CACHE KV. Bump
|
|
10
|
+
* it (see api/cache-purge.ts, called by the ETL after a re-migration) and every
|
|
11
|
+
* old key becomes unreachable — tag-like purge with no URL enumeration, and no
|
|
12
|
+
* dependency on Enterprise cache-tags. Stale keys age out on their own.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately NOT cached: preview/test output (per-developer, noindex), any
|
|
15
|
+
* response with Set-Cookie, and anything non-200/non-HTML. In `astro dev` there
|
|
16
|
+
* is no `caches` global, so every helper is a clean no-op.
|
|
17
|
+
*
|
|
18
|
+
* Note on the CSP nonce: a stored page carries the nonce it was rendered with, in
|
|
19
|
+
* both its inline tags and its CSP header, and is served to many users. That is
|
|
20
|
+
* safe — the nonce is not a secret; it only has to match within one identical,
|
|
21
|
+
* non-personalized document, which it does.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Synthetic internal origin for cache keys — never hits the network. */
|
|
25
|
+
const CACHE_ORIGIN = "https://page-cache.tot.internal";
|
|
26
|
+
const versionKey = (tenantId: string) => `cachever:${tenantId}`;
|
|
27
|
+
|
|
28
|
+
/** Default freshness window (s-maxage) and stale window for cached pages. */
|
|
29
|
+
const DEFAULT_TTL = 300;
|
|
30
|
+
const DEFAULT_SWR = 86_400;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Routes worth caching. Excludes the JSON typeahead (`?format=json`, high-q
|
|
34
|
+
* cardinality), `/api/*`, and anything with a preview token (handled upstream).
|
|
35
|
+
* Single-segment slugs (e.g. /about, /shipping-returns) cover editorial pages;
|
|
36
|
+
* non-200 responses are filtered at store time, so unknown slugs never persist.
|
|
37
|
+
*/
|
|
38
|
+
export function isCacheablePath(pathname: string, params: URLSearchParams): boolean {
|
|
39
|
+
if (params.get("format") === "json") return false;
|
|
40
|
+
if (pathname.startsWith("/api/")) return false;
|
|
41
|
+
// Authenticated operator surfaces are per-viewer and MUST NOT be edge-cached —
|
|
42
|
+
// the bare `/dashboard` index is a single-segment slug that would otherwise
|
|
43
|
+
// match the editorial-page rule below and be served across viewers.
|
|
44
|
+
if (pathname === "/dashboard" || pathname.startsWith("/dashboard/")) return false;
|
|
45
|
+
// `/dev` is the authenticated developer surface — per-viewer (it renders the
|
|
46
|
+
// signed-in developer's own store, email, and gated demo), so like /dashboard
|
|
47
|
+
// it must never be shared across viewers. It's also the loudest failure if
|
|
48
|
+
// stale: its styles build to a CONTENT-HASHED asset (/_astro/dev.<hash>.css),
|
|
49
|
+
// so a cached old HTML keeps pointing at a hash that no longer exists after the
|
|
50
|
+
// next deploy → the stylesheet 404s and the page renders unstyled (its inline
|
|
51
|
+
// nonce'd <script> tags break the same way). Never cache it.
|
|
52
|
+
if (pathname === "/dev") return false;
|
|
53
|
+
if (pathname === "/" || pathname === "/search") return true;
|
|
54
|
+
if (pathname.startsWith("/products/")) return true;
|
|
55
|
+
if (pathname.startsWith("/collections/")) return true;
|
|
56
|
+
return /^\/[a-z0-9][a-z0-9-]*\/?$/.test(pathname);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function pageCacheKey(
|
|
60
|
+
tenantId: string,
|
|
61
|
+
pathname: string,
|
|
62
|
+
search: string,
|
|
63
|
+
version: number,
|
|
64
|
+
): Request {
|
|
65
|
+
return new Request(`${CACHE_ORIGIN}/${tenantId}/v${version}${pathname}${search}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Current cache version for a tenant (defaults to 1). KV read is edge-cached. */
|
|
69
|
+
export async function readCacheVersion(
|
|
70
|
+
kv: KVNamespace,
|
|
71
|
+
tenantId: string,
|
|
72
|
+
): Promise<number> {
|
|
73
|
+
const raw = await kv.get(versionKey(tenantId), { cacheTtl: 60 });
|
|
74
|
+
const n = raw ? Number.parseInt(raw, 10) : Number.NaN;
|
|
75
|
+
return Number.isFinite(n) && n > 0 ? n : 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Bump a tenant's cache version (logical purge of all its pages). Returns the new version. */
|
|
79
|
+
export async function bumpCacheVersion(
|
|
80
|
+
kv: KVNamespace,
|
|
81
|
+
tenantId: string,
|
|
82
|
+
): Promise<number> {
|
|
83
|
+
const next = (await readCacheVersion(kv, tenantId)) + 1;
|
|
84
|
+
await kv.put(versionKey(tenantId), String(next));
|
|
85
|
+
return next;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Return a cached page response (fresh clone), or null on miss. */
|
|
89
|
+
export async function getCachedPage(
|
|
90
|
+
cache: Cache,
|
|
91
|
+
key: Request,
|
|
92
|
+
): Promise<Response | null> {
|
|
93
|
+
const hit = await cache.match(key);
|
|
94
|
+
if (!hit) return null;
|
|
95
|
+
const res = new Response(hit.body, hit);
|
|
96
|
+
res.headers.set("X-ToT-Cache", "hit");
|
|
97
|
+
return res;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Store a rendered page if it is safe to cache. No-op for non-200, non-HTML, or
|
|
102
|
+
* cookie-bearing responses. Sets a public Cache-Control so any fronting CDN can
|
|
103
|
+
* help too. Caller passes the FINAL response (after CSP headers are set).
|
|
104
|
+
*/
|
|
105
|
+
export async function putCachedPage(
|
|
106
|
+
cache: Cache,
|
|
107
|
+
key: Request,
|
|
108
|
+
response: Response,
|
|
109
|
+
opts: { ttl?: number; swr?: number } = {},
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
if (response.status !== 200) return;
|
|
112
|
+
if (!(response.headers.get("content-type") ?? "").includes("text/html")) return;
|
|
113
|
+
if (response.headers.has("set-cookie")) return;
|
|
114
|
+
|
|
115
|
+
const ttl = opts.ttl ?? DEFAULT_TTL;
|
|
116
|
+
const swr = opts.swr ?? DEFAULT_SWR;
|
|
117
|
+
const cloned = response.clone();
|
|
118
|
+
const toStore = new Response(cloned.body, cloned);
|
|
119
|
+
toStore.headers.set(
|
|
120
|
+
"Cache-Control",
|
|
121
|
+
`public, s-maxage=${ttl}, stale-while-revalidate=${swr}`,
|
|
122
|
+
);
|
|
123
|
+
toStore.headers.set("X-ToT-Cache", "stored");
|
|
124
|
+
await cache.put(key, toStore).catch(() => void 0);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const PAGE_CACHE_TTL = DEFAULT_TTL;
|
|
128
|
+
export const PAGE_CACHE_SWR = DEFAULT_SWR;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The storefront's OWN branded developer-invite email (magic-link flow). The storefront
|
|
3
|
+
* owns presentation now — this is our template, our brand, rendered here and handed to
|
|
4
|
+
* tot20 only for transport (see lib/messaging/messagesClient.ts). A single primary CTA
|
|
5
|
+
* links to the GET-safe /auth/magic landing carrying the one-time token.
|
|
6
|
+
*
|
|
7
|
+
* Email HTML rules: inline styles only (no <style>/external CSS — mail clients strip them),
|
|
8
|
+
* table-free simple layout, and a visible fallback URL under the button (some clients don't
|
|
9
|
+
* render buttons). The token rides in the URL but is only consumed by a POST on the landing
|
|
10
|
+
* page, so a scanner GET can't burn it.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Minimal HTML escape for untrusted interpolation (the inviter note). */
|
|
14
|
+
function esc(s: string): string {
|
|
15
|
+
return s
|
|
16
|
+
.replace(/&/g, "&")
|
|
17
|
+
.replace(/</g, "<")
|
|
18
|
+
.replace(/>/g, ">")
|
|
19
|
+
.replace(/"/g, """);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MagicLinkInviteEmailInput {
|
|
23
|
+
/** The full magic-link URL (…/auth/magic?token=…) — already token-carrying. */
|
|
24
|
+
magicUrl: string;
|
|
25
|
+
/** Display name of the store/experience. Default "Regulated Storefront". */
|
|
26
|
+
storeName?: string;
|
|
27
|
+
/** Optional personal note from the inviter (untrusted → escaped). */
|
|
28
|
+
note?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function renderMagicLinkInviteEmail(input: MagicLinkInviteEmailInput): {
|
|
32
|
+
subject: string;
|
|
33
|
+
html: string;
|
|
34
|
+
} {
|
|
35
|
+
const store = input.storeName?.trim() || "Regulated Storefront";
|
|
36
|
+
const subject = `Your invitation to ${store}`;
|
|
37
|
+
const url = input.magicUrl;
|
|
38
|
+
const note = input.note?.trim() ? esc(input.note.trim()) : null;
|
|
39
|
+
|
|
40
|
+
const html = `<!doctype html>
|
|
41
|
+
<html lang="en">
|
|
42
|
+
<head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /></head>
|
|
43
|
+
<body style="margin:0;padding:0;background:#f5f4f0;">
|
|
44
|
+
<div style="max-width:520px;margin:0 auto;padding:32px 20px;font:16px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#1c1c1c;">
|
|
45
|
+
<div style="font:600 12px/1 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.12em;text-transform:uppercase;color:#0f7b6c;margin:0 0 24px;">
|
|
46
|
+
Token of Trust© · ${esc(store)}
|
|
47
|
+
</div>
|
|
48
|
+
<h1 style="font-size:22px;line-height:1.3;letter-spacing:-.01em;margin:0 0 12px;color:#0b1f3a;">You've been invited</h1>
|
|
49
|
+
<p style="margin:0 0 8px;color:#3a3a3a;">You've been given developer access to <strong>${esc(store)}</strong>. Click below to sign in — no password needed.</p>
|
|
50
|
+
${note ? `<div style="margin:16px 0;padding:12px 14px;background:#eef4ff;border:1px solid #c3d4f5;border-radius:8px;color:#234;font-size:14px;"><strong>Note from your inviter:</strong><br>${note}</div>` : ""}
|
|
51
|
+
<div style="margin:28px 0;">
|
|
52
|
+
<a href="${url}" style="display:inline-block;background:#0f7b6c;color:#ffffff;text-decoration:none;font-weight:600;font-size:16px;padding:14px 28px;border-radius:9px;">Sign in to ${esc(store)}</a>
|
|
53
|
+
</div>
|
|
54
|
+
<p style="margin:0 0 4px;font-size:13px;color:#6a6a6a;">Or paste this link into your browser:</p>
|
|
55
|
+
<p style="margin:0 0 24px;font-size:13px;word-break:break-all;"><a href="${url}" style="color:#0f7b6c;">${url}</a></p>
|
|
56
|
+
<p style="margin:0;font-size:12px;color:#9a9a9a;border-top:1px solid #e6e4de;padding-top:16px;">This is a single-use sign-in link. If you didn't expect this invitation, you can ignore this email.</p>
|
|
57
|
+
</div>
|
|
58
|
+
</body>
|
|
59
|
+
</html>`;
|
|
60
|
+
|
|
61
|
+
return { subject, html };
|
|
62
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime env access that works in BOTH contexts:
|
|
3
|
+
* - `astro dev` (Node): reads import.meta.env / process.env.
|
|
4
|
+
* - Cloudflare Workers (prod): reads the `cloudflare:workers` env module.
|
|
5
|
+
*
|
|
6
|
+
* Astro v6+/@astrojs/cloudflare v14 REMOVED `Astro.locals.runtime.env`; the
|
|
7
|
+
* supported path is `import { env } from "cloudflare:workers"`. That module only
|
|
8
|
+
* exists in the worker runtime, so we import it lazily + guarded and fall back
|
|
9
|
+
* to build-time env in dev. See DECISIONS.md.
|
|
10
|
+
*/
|
|
11
|
+
import type { D1Like } from "./d1/catalog.js";
|
|
12
|
+
|
|
13
|
+
let workerEnv: Record<string, unknown> | null | undefined;
|
|
14
|
+
|
|
15
|
+
async function getWorkerEnv(): Promise<Record<string, unknown> | null> {
|
|
16
|
+
if (workerEnv !== undefined) return workerEnv;
|
|
17
|
+
try {
|
|
18
|
+
// @ts-expect-error — virtual module, only resolvable in the Workers runtime.
|
|
19
|
+
const mod = await import("cloudflare:workers");
|
|
20
|
+
workerEnv = (mod.env ?? null) as Record<string, unknown> | null;
|
|
21
|
+
} catch {
|
|
22
|
+
workerEnv = null;
|
|
23
|
+
}
|
|
24
|
+
return workerEnv;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function fromBuild(key: string): string | undefined {
|
|
28
|
+
const meta = import.meta.env as Record<string, string | undefined>;
|
|
29
|
+
if (meta[key] != null) return meta[key];
|
|
30
|
+
if (typeof process !== "undefined" && process.env && process.env[key] != null) {
|
|
31
|
+
return process.env[key];
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function readEnv(key: string): Promise<string | undefined> {
|
|
37
|
+
const w = await getWorkerEnv();
|
|
38
|
+
const wv = w?.[key];
|
|
39
|
+
if (typeof wv === "string" && wv.length) return wv;
|
|
40
|
+
return fromBuild(key);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** KV binding for the newsletter stub / tenant cache, if present in the runtime. */
|
|
44
|
+
export async function readKv(binding: string): Promise<KVNamespace | undefined> {
|
|
45
|
+
const w = await getWorkerEnv();
|
|
46
|
+
const b = w?.[binding];
|
|
47
|
+
return (b as KVNamespace | undefined) ?? undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** D1 catalog binding for the edge read replica, if present in the runtime. */
|
|
51
|
+
export async function readD1(binding: string): Promise<D1Like | undefined> {
|
|
52
|
+
const w = await getWorkerEnv();
|
|
53
|
+
return (w?.[binding] as D1Like | undefined) ?? undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** R2 bucket binding (e.g. TENANT_ASSETS, PRODUCT_IMAGES), if present. */
|
|
57
|
+
export async function readR2(binding: string): Promise<R2Bucket | undefined> {
|
|
58
|
+
const w = await getWorkerEnv();
|
|
59
|
+
return (w?.[binding] as R2Bucket | undefined) ?? undefined;
|
|
60
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture CatalogProducts for the style guide / component previews. Hard-coded so
|
|
3
|
+
* the commerce components render WITHOUT a live catalog. Each entry exercises a
|
|
4
|
+
* different card/price/badge state (default, sale, from-price, out-of-stock,
|
|
5
|
+
* compliant, new). Preview-only — never served on a real storefront surface.
|
|
6
|
+
*/
|
|
7
|
+
import type { CatalogProduct, CatalogVariant } from "@tot/public-runtime";
|
|
8
|
+
|
|
9
|
+
const NOW = "2026-07-01T00:00:00Z";
|
|
10
|
+
|
|
11
|
+
interface VariantSeed {
|
|
12
|
+
price: number;
|
|
13
|
+
compareAt?: number;
|
|
14
|
+
available?: boolean;
|
|
15
|
+
option?: Record<string, string>;
|
|
16
|
+
title?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function mkVariant(handle: string, i: number, v: VariantSeed): CatalogVariant {
|
|
20
|
+
return {
|
|
21
|
+
tenant_id: "sample",
|
|
22
|
+
id: `${handle}-v${i}`,
|
|
23
|
+
product_id: handle,
|
|
24
|
+
source: { platform: "shopify", source_id: `${handle}-v${i}` },
|
|
25
|
+
sku: `${handle.toUpperCase()}-${i}`,
|
|
26
|
+
title: v.title ?? "Default",
|
|
27
|
+
price: v.price,
|
|
28
|
+
compare_at_price: v.compareAt,
|
|
29
|
+
currency: "USD",
|
|
30
|
+
option_values: v.option ?? {},
|
|
31
|
+
inventory: { available: v.available ?? true },
|
|
32
|
+
position: i + 1,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ProductSeed {
|
|
37
|
+
handle: string;
|
|
38
|
+
title: string;
|
|
39
|
+
vendor?: string;
|
|
40
|
+
tags?: string[];
|
|
41
|
+
createdAt?: string;
|
|
42
|
+
options?: CatalogProduct["options"];
|
|
43
|
+
variants: VariantSeed[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function mkProduct(seed: ProductSeed): CatalogProduct {
|
|
47
|
+
const variants = seed.variants.map((v, i) => mkVariant(seed.handle, i, v));
|
|
48
|
+
const prices = variants.map((v) => v.price);
|
|
49
|
+
return {
|
|
50
|
+
tenant_id: "sample",
|
|
51
|
+
id: seed.handle,
|
|
52
|
+
source: { platform: "shopify", source_id: seed.handle },
|
|
53
|
+
handle: seed.handle,
|
|
54
|
+
title: seed.title,
|
|
55
|
+
description_html: `<p>${seed.title}</p>`,
|
|
56
|
+
description_text: seed.title,
|
|
57
|
+
vendor: seed.vendor,
|
|
58
|
+
product_type: "Sample",
|
|
59
|
+
tags: seed.tags ?? [],
|
|
60
|
+
status: "active",
|
|
61
|
+
seo: {},
|
|
62
|
+
options: seed.options ?? [],
|
|
63
|
+
images: [],
|
|
64
|
+
variants,
|
|
65
|
+
price_range: { min: Math.min(...prices), max: Math.max(...prices), currency: "USD" },
|
|
66
|
+
collections: ["sample"],
|
|
67
|
+
metafields: [],
|
|
68
|
+
created_at: seed.createdAt ?? NOW,
|
|
69
|
+
updated_at: NOW,
|
|
70
|
+
migrated_at: NOW,
|
|
71
|
+
source_checksum: seed.handle,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Six products, each showing a different card/price/badge permutation. */
|
|
76
|
+
export const sampleProducts: CatalogProduct[] = [
|
|
77
|
+
mkProduct({
|
|
78
|
+
handle: "sample-default",
|
|
79
|
+
title: "Classic Everyday Tee",
|
|
80
|
+
vendor: "Northwind",
|
|
81
|
+
tags: ["bestseller"],
|
|
82
|
+
variants: [{ price: 28 }],
|
|
83
|
+
}),
|
|
84
|
+
mkProduct({
|
|
85
|
+
handle: "sample-sale",
|
|
86
|
+
title: "Merino Zip Hoodie",
|
|
87
|
+
vendor: "Northwind",
|
|
88
|
+
tags: ["bestseller"],
|
|
89
|
+
variants: [{ price: 64, compareAt: 96 }],
|
|
90
|
+
}),
|
|
91
|
+
mkProduct({
|
|
92
|
+
handle: "sample-from",
|
|
93
|
+
title: "Trail Runner — All Sizes",
|
|
94
|
+
vendor: "Summit Co.",
|
|
95
|
+
options: [{ name: "Size", position: 1, values: ["S", "M", "L"] }],
|
|
96
|
+
variants: [
|
|
97
|
+
{ price: 42, option: { Size: "S" }, title: "S" },
|
|
98
|
+
{ price: 48, option: { Size: "M" }, title: "M" },
|
|
99
|
+
{ price: 55, option: { Size: "L" }, title: "L" },
|
|
100
|
+
],
|
|
101
|
+
}),
|
|
102
|
+
mkProduct({
|
|
103
|
+
handle: "sample-oos",
|
|
104
|
+
title: "Limited Canvas Pack",
|
|
105
|
+
vendor: "Field & Co.",
|
|
106
|
+
variants: [{ price: 120, available: false }],
|
|
107
|
+
}),
|
|
108
|
+
mkProduct({
|
|
109
|
+
handle: "sample-compliant",
|
|
110
|
+
title: "Age-Verified Reserve",
|
|
111
|
+
vendor: "Cellar 21",
|
|
112
|
+
tags: ["compliant", "bestseller"],
|
|
113
|
+
variants: [{ price: 89 }],
|
|
114
|
+
}),
|
|
115
|
+
mkProduct({
|
|
116
|
+
handle: "sample-new",
|
|
117
|
+
title: "Just-Dropped Utility Cap",
|
|
118
|
+
vendor: "Northwind",
|
|
119
|
+
tags: ["new"],
|
|
120
|
+
createdAt: NOW,
|
|
121
|
+
variants: [{ price: 24, compareAt: 32 }],
|
|
122
|
+
}),
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
/** Optional per-handle rating aggregates for the preview (source before use). */
|
|
126
|
+
export const sampleRatings: Record<string, { rating: number; count: number }> = {
|
|
127
|
+
"sample-default": { rating: 4.5, count: 128 },
|
|
128
|
+
"sample-sale": { rating: 4.8, count: 64 },
|
|
129
|
+
"sample-compliant": { rating: 5, count: 12 },
|
|
130
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Small presentation helpers shared across components. Tenant locale/currency aware. */
|
|
2
|
+
|
|
3
|
+
export function formatPrice(
|
|
4
|
+
amount: number,
|
|
5
|
+
currency: string,
|
|
6
|
+
locale = "en-US",
|
|
7
|
+
): string {
|
|
8
|
+
try {
|
|
9
|
+
return new Intl.NumberFormat(locale, {
|
|
10
|
+
style: "currency",
|
|
11
|
+
currency,
|
|
12
|
+
minimumFractionDigits: amount % 1 === 0 ? 0 : 2,
|
|
13
|
+
}).format(amount);
|
|
14
|
+
} catch {
|
|
15
|
+
return `${currency} ${amount.toFixed(2)}`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function discountPercent(price: number, compareAt?: number): number | null {
|
|
20
|
+
if (!compareAt || compareAt <= price) return null;
|
|
21
|
+
return Math.round(((compareAt - price) / compareAt) * 100);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Truncate plain text for meta descriptions / card copy. */
|
|
25
|
+
export function truncate(text: string, max = 160): string {
|
|
26
|
+
const clean = text.replace(/\s+/g, " ").trim();
|
|
27
|
+
if (clean.length <= max) return clean;
|
|
28
|
+
return clean.slice(0, max - 1).replace(/\s+\S*$/, "") + "…";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Locale-aware "Jan 3, 2026" formatting for an ISO date; "" if missing/invalid. */
|
|
32
|
+
export function formatDate(iso: string | undefined, locale = "en-US"): string {
|
|
33
|
+
if (!iso) return "";
|
|
34
|
+
const d = new Date(iso);
|
|
35
|
+
return Number.isNaN(d.getTime())
|
|
36
|
+
? ""
|
|
37
|
+
: d.toLocaleDateString(locale, { year: "numeric", month: "short", day: "numeric" });
|
|
38
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-scoped FoxyCart commerce resolution (Track A).
|
|
3
|
+
*
|
|
4
|
+
* Bridges the tenant's static `commerce.foxy` config with runtime env so the
|
|
5
|
+
* demo store can be swapped without a code change and the signing secret stays
|
|
6
|
+
* out of source:
|
|
7
|
+
* - `FOXY_STORE_SUBDOMAIN` overrides the tenant's default subdomain (one env
|
|
8
|
+
* var points every commerce tenant at our shared demo store).
|
|
9
|
+
* - `FOXY_CART_SECRET` is the cart-validation secret used only in `signed`
|
|
10
|
+
* mode; the `unsigned-demo` mode never reads it (validation is OFF), which
|
|
11
|
+
* is what lets a developer run the whole cart locally with zero secrets.
|
|
12
|
+
*/
|
|
13
|
+
import type { TenantConfig, ResolvedFoxyCommerce } from "@tot/public-runtime";
|
|
14
|
+
import { resolveFoxyCommerce } from "@tot/public-runtime";
|
|
15
|
+
import { readEnv } from "@/lib/env";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the effective FoxyCart facts for a tenant, or null when the tenant has
|
|
19
|
+
* no store configured. Applies the `FOXY_STORE_SUBDOMAIN` override.
|
|
20
|
+
*/
|
|
21
|
+
export async function resolveTenantCommerce(
|
|
22
|
+
tenant: TenantConfig,
|
|
23
|
+
): Promise<ResolvedFoxyCommerce | null> {
|
|
24
|
+
const foxy = tenant.commerce?.foxy;
|
|
25
|
+
if (!foxy) return null;
|
|
26
|
+
// Per-environment overrides so the SAME tenant can point at different stores /
|
|
27
|
+
// checkout hosts by env: FOXY_STORE_SUBDOMAIN picks the Foxy store; FOXY_STORE_
|
|
28
|
+
// DOMAIN sets a branded custom checkout domain (e.g. checkout.preview.
|
|
29
|
+
// tokenoftrust.com on the shared test env, secure.<merchant>.com on live).
|
|
30
|
+
const subOverride = await readEnv("FOXY_STORE_SUBDOMAIN");
|
|
31
|
+
const domainOverride = await readEnv("FOXY_STORE_DOMAIN");
|
|
32
|
+
const storeSubDomain =
|
|
33
|
+
subOverride && subOverride.length ? subOverride : foxy.storeSubDomain;
|
|
34
|
+
const storeDomain =
|
|
35
|
+
domainOverride && domainOverride.length ? domainOverride : foxy.storeDomain;
|
|
36
|
+
return resolveFoxyCommerce({ ...foxy, storeSubDomain, storeDomain });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The cart-validation secret for signed mode. Returns undefined in
|
|
41
|
+
* `unsigned-demo` mode (never needed) or when unset. A signed-mode tenant with
|
|
42
|
+
* no secret is a misconfiguration the caller must handle (fall back to no cart).
|
|
43
|
+
*/
|
|
44
|
+
export async function readCartSecret(
|
|
45
|
+
commerce: ResolvedFoxyCommerce,
|
|
46
|
+
): Promise<string | undefined> {
|
|
47
|
+
if (commerce.mode !== "signed") return undefined;
|
|
48
|
+
return readEnv("FOXY_CART_SECRET");
|
|
49
|
+
}
|