@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,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signed storefront → FoxyCart theme-publish client (U11b, the PUSH half of U11).
|
|
3
|
+
*
|
|
4
|
+
* Points a store's Foxy checkout template at the storefront-hosted stylesheet
|
|
5
|
+
* (`GET /api/checkout-style.css`, U11) by driving tot-foxycart's `/theme/foxy`
|
|
6
|
+
* endpoint: PUT the `content_url`, then POST `/refresh` (PUT alone only sets the
|
|
7
|
+
* pointer — Foxy caches the template, so nothing goes live until refresh).
|
|
8
|
+
*
|
|
9
|
+
* AUTH: each request carries an `x-tot-signature` header = lowercase-hex
|
|
10
|
+
* HMAC-SHA256 of `JSON.stringify(body)` keyed by the STORE'S ToT SECRET KEY —
|
|
11
|
+
* byte-identical to tot-foxycart's `foxyStore.verifyWebhookSignature`. That
|
|
12
|
+
* secret is the storefront's `TOT_SECRET_KEY`; this is independent of the
|
|
13
|
+
* FoxyCart hAPI credentials (see decision `u11b-not-blocked-by-u7`).
|
|
14
|
+
*
|
|
15
|
+
* Edge-safe + node-free (WebCrypto HMAC); the network call is injected so the
|
|
16
|
+
* flow is fully unit-testable offline. The caller (the checkout-styling promote
|
|
17
|
+
* hook — U13) supplies the resolved secret + base URL from server context; the
|
|
18
|
+
* secret is never shipped to the client.
|
|
19
|
+
*/
|
|
20
|
+
import type { CustomizationEnv } from "./customization-preview.js";
|
|
21
|
+
|
|
22
|
+
/** Header tot-foxycart reads the HMAC from (matches foxyThemeEndpoint SIGNATURE_HEADER). */
|
|
23
|
+
export const TOT_SIGNATURE_HEADER = "x-tot-signature";
|
|
24
|
+
|
|
25
|
+
/** Foxy caps content_url at 300 chars; '' detaches the template (revert to default). */
|
|
26
|
+
export const MAX_CONTENT_URL = 300;
|
|
27
|
+
|
|
28
|
+
/** Fetch shape we depend on — the platform `fetch`, injectable for tests. */
|
|
29
|
+
export type FetchLike = (
|
|
30
|
+
input: string,
|
|
31
|
+
init?: {
|
|
32
|
+
method?: string;
|
|
33
|
+
headers?: Record<string, string>;
|
|
34
|
+
body?: string;
|
|
35
|
+
},
|
|
36
|
+
) => Promise<{ ok: boolean; status: number; text: () => Promise<string> }>;
|
|
37
|
+
|
|
38
|
+
export interface ThemePublishTarget {
|
|
39
|
+
/** tot-foxycart proxy base, e.g. `https://foxycart-test.tokenoftrust.com`. */
|
|
40
|
+
foxyBaseUrl: string;
|
|
41
|
+
/** The store's app domain / subdomain (tot-foxycart resolves the store from it). */
|
|
42
|
+
appDomain: string;
|
|
43
|
+
/** The hosted stylesheet URL Foxy will fetch (U11's `/api/checkout-style.css`). */
|
|
44
|
+
contentUrl: string;
|
|
45
|
+
/** The store's ToT secret key (storefront `TOT_SECRET_KEY`) — signs the request. */
|
|
46
|
+
secret: string;
|
|
47
|
+
/** Foxy template to target; checkout styling uses `checkout` (the default). */
|
|
48
|
+
templateType?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ThemePublishResult {
|
|
52
|
+
ok: boolean;
|
|
53
|
+
/** Parsed PUT response (pointer set), when it succeeded. */
|
|
54
|
+
set?: unknown;
|
|
55
|
+
/** Parsed refresh response (cache re-pulled), when it succeeded. */
|
|
56
|
+
refreshed?: boolean;
|
|
57
|
+
/** Failure detail: which step + the endpoint's status/message. */
|
|
58
|
+
error?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Lowercase-hex HMAC-SHA256(message) under `secret` (WebCrypto). Matches foxy.ts. */
|
|
62
|
+
async function hmacHex(secret: string, message: string): Promise<string> {
|
|
63
|
+
const enc = new TextEncoder();
|
|
64
|
+
const key = await crypto.subtle.importKey(
|
|
65
|
+
"raw",
|
|
66
|
+
enc.encode(secret),
|
|
67
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
68
|
+
false,
|
|
69
|
+
["sign"],
|
|
70
|
+
);
|
|
71
|
+
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(message));
|
|
72
|
+
return [...new Uint8Array(sig)]
|
|
73
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
74
|
+
.join("");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The exact signature tot-foxycart verifies: hex HMAC-SHA256 of the JSON body
|
|
79
|
+
* string under the store secret. Exported so the promote hook and tests can
|
|
80
|
+
* compute/assert it identically.
|
|
81
|
+
*/
|
|
82
|
+
export function signThemeRequestBody(
|
|
83
|
+
secret: string,
|
|
84
|
+
body: unknown,
|
|
85
|
+
): Promise<string> {
|
|
86
|
+
return hmacHex(secret, JSON.stringify(body));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The canonical, stable content_url a store's checkout template points at — the
|
|
91
|
+
* U11 route on the store's own host, so Foxy's server-side fetch self-identifies
|
|
92
|
+
* the tenant + env. Returns null if it would exceed Foxy's 300-char cap (so the
|
|
93
|
+
* caller surfaces a config error rather than pushing a URL Foxy will reject).
|
|
94
|
+
*/
|
|
95
|
+
export function checkoutStyleContentUrl(
|
|
96
|
+
host: string,
|
|
97
|
+
basePath = "",
|
|
98
|
+
): string | null {
|
|
99
|
+
const url = `https://${host}${basePath}/api/checkout-style.css`;
|
|
100
|
+
return url.length <= MAX_CONTENT_URL ? url : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function readErr(
|
|
104
|
+
res: { status: number; text: () => Promise<string> },
|
|
105
|
+
step: string,
|
|
106
|
+
): Promise<string> {
|
|
107
|
+
let detail = "";
|
|
108
|
+
try {
|
|
109
|
+
detail = (await res.text()).slice(0, 300);
|
|
110
|
+
} catch {
|
|
111
|
+
/* ignore */
|
|
112
|
+
}
|
|
113
|
+
return `${step} failed (HTTP ${res.status})${detail ? `: ${detail}` : ""}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Publish a store's checkout styling to Foxy: signed PUT `/theme/foxy` then
|
|
118
|
+
* signed POST `/theme/foxy/refresh`. Returns a discriminated result — we do NOT
|
|
119
|
+
* fail open: if the PUT fails we skip the refresh and report the error, so the
|
|
120
|
+
* caller never reports "published" for something that didn't take.
|
|
121
|
+
*
|
|
122
|
+
* `fetchImpl` defaults to the global `fetch`; tests inject a stub.
|
|
123
|
+
*/
|
|
124
|
+
export async function publishCheckoutTheme(
|
|
125
|
+
target: ThemePublishTarget,
|
|
126
|
+
fetchImpl?: FetchLike,
|
|
127
|
+
): Promise<ThemePublishResult> {
|
|
128
|
+
const doFetch = fetchImpl ?? (globalThis.fetch as unknown as FetchLike);
|
|
129
|
+
const templateType = target.templateType ?? "checkout";
|
|
130
|
+
const base = target.foxyBaseUrl.replace(/\/+$/, "");
|
|
131
|
+
|
|
132
|
+
if (
|
|
133
|
+
target.contentUrl !== "" &&
|
|
134
|
+
(!target.contentUrl.startsWith("https://") ||
|
|
135
|
+
target.contentUrl.length > MAX_CONTENT_URL)
|
|
136
|
+
) {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
error: `invalid contentUrl (must be https and <= ${MAX_CONTENT_URL} chars)`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 1. PUT the pointer.
|
|
144
|
+
const setBody = {
|
|
145
|
+
appDomain: target.appDomain,
|
|
146
|
+
templateType,
|
|
147
|
+
contentUrl: target.contentUrl,
|
|
148
|
+
};
|
|
149
|
+
const setSig = await signThemeRequestBody(target.secret, setBody);
|
|
150
|
+
const setRes = await doFetch(`${base}/theme/foxy`, {
|
|
151
|
+
method: "PUT",
|
|
152
|
+
headers: {
|
|
153
|
+
"content-type": "application/json",
|
|
154
|
+
[TOT_SIGNATURE_HEADER]: setSig,
|
|
155
|
+
},
|
|
156
|
+
body: JSON.stringify(setBody),
|
|
157
|
+
});
|
|
158
|
+
if (!setRes.ok) {
|
|
159
|
+
return { ok: false, error: await readErr(setRes, "PUT /theme/foxy") };
|
|
160
|
+
}
|
|
161
|
+
let set: unknown = undefined;
|
|
162
|
+
try {
|
|
163
|
+
set = JSON.parse(await setRes.text());
|
|
164
|
+
} catch {
|
|
165
|
+
/* non-JSON ok-body — leave undefined */
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 2. POST refresh to make it live.
|
|
169
|
+
const refreshBody = { appDomain: target.appDomain, templateType };
|
|
170
|
+
const refreshSig = await signThemeRequestBody(target.secret, refreshBody);
|
|
171
|
+
const refreshRes = await doFetch(`${base}/theme/foxy/refresh`, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: {
|
|
174
|
+
"content-type": "application/json",
|
|
175
|
+
[TOT_SIGNATURE_HEADER]: refreshSig,
|
|
176
|
+
},
|
|
177
|
+
body: JSON.stringify(refreshBody),
|
|
178
|
+
});
|
|
179
|
+
if (!refreshRes.ok) {
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
set,
|
|
183
|
+
error: await readErr(refreshRes, "POST /theme/foxy/refresh"),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { ok: true, set, refreshed: true };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Convenience: the env → Foxy-proxy base URL for this workstream's targets.
|
|
192
|
+
* qa runs NODE_ENV=test (proxy `foxycart-test`); sandbox → `foxycart-sandbox`;
|
|
193
|
+
* live → the prod proxy. Kept here so the promote hook (U13) resolves one place.
|
|
194
|
+
*/
|
|
195
|
+
export function foxyProxyBaseUrl(env: CustomizationEnv): string {
|
|
196
|
+
// "test" is the qa target's env; "live" is prod.
|
|
197
|
+
return env === "live"
|
|
198
|
+
? "https://foxycart.tokenoftrust.com"
|
|
199
|
+
: "https://foxycart-test.tokenoftrust.com";
|
|
200
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkout-styling data model + published-artifact read plumbing.
|
|
3
|
+
*
|
|
4
|
+
* The FoxyCart-hosted checkout is styled by CSS pushed to Foxy's `/theme/foxy`
|
|
5
|
+
* endpoint (U11). This module owns the *source of truth* for that styling: a
|
|
6
|
+
* small, structured, tenant-editable descriptor that later units render into
|
|
7
|
+
* ASCII-safe CSS. It sits on the SAME versioned publish spine as theme/content
|
|
8
|
+
* (D1VersionStore + KV pointers) — resolution is per-environment via
|
|
9
|
+
* `readPublishedArtifact`, so a merchant's draft previews on `test` and the
|
|
10
|
+
* promoted style serves on `live`, exactly like the theme artifact.
|
|
11
|
+
*
|
|
12
|
+
* Two colocated artifacts under `tenants/<id>/`:
|
|
13
|
+
* - `checkout-style.json` — the structured `CheckoutStyle` descriptor (below).
|
|
14
|
+
* - `checkout.css` — an optional raw-CSS escape hatch a merchant edits
|
|
15
|
+
* directly; hosted + pushed to Foxy as-is (U11).
|
|
16
|
+
*
|
|
17
|
+
* Pure + dependency-free (node-free, edge-safe): the caller injects the same
|
|
18
|
+
* `KvGet` used for theme/content, keeping this unit-testable and storage-agnostic.
|
|
19
|
+
* No UI here — that's U12.
|
|
20
|
+
*/
|
|
21
|
+
import type { CustomizationEnv } from "./customization-preview.js";
|
|
22
|
+
import type { DeepPartial, ThemeTokens } from "./tenant.js";
|
|
23
|
+
import { readPublishedArtifact, type KvGet } from "./customization-runtime.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Color levers exposed on the checkout page. Deliberately a small, opinionated
|
|
27
|
+
* subset of the full theme palette — the checkout is a focused conversion
|
|
28
|
+
* surface, not the whole store. All values are CSS colors; hex is validated for
|
|
29
|
+
* WCAG contrast downstream (U13 gate), non-hex (e.g. `rgb(...)`) is passed
|
|
30
|
+
* through. Keep values ASCII-safe — Foxy's cache pass isn't Unicode-safe.
|
|
31
|
+
*/
|
|
32
|
+
export interface CheckoutStyleColors {
|
|
33
|
+
/** Page background behind the checkout card. */
|
|
34
|
+
background: string;
|
|
35
|
+
/** Surface color of the checkout card / form panels. */
|
|
36
|
+
surface: string;
|
|
37
|
+
/** Primary brand color — section headings and the pay button fill. */
|
|
38
|
+
primary: string;
|
|
39
|
+
/** Foreground on `primary` (button label / on-primary text) — contrast pair. */
|
|
40
|
+
primaryContrast: string;
|
|
41
|
+
/** Accent color — links, focus rings, selected states. */
|
|
42
|
+
accent: string;
|
|
43
|
+
/** Body text color. */
|
|
44
|
+
text: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Typography for the checkout page. `fontFamily` is a full CSS font-family stack
|
|
49
|
+
* (a web-safe stack, or a Foxy-supported webfont name plus fallbacks) — kept as
|
|
50
|
+
* an opaque string so a merchant can bring their own stack without a schema bump.
|
|
51
|
+
*/
|
|
52
|
+
export interface CheckoutStyleTypography {
|
|
53
|
+
/** CSS font-family stack, e.g. `"Inter", system-ui, sans-serif`. */
|
|
54
|
+
fontFamily: string;
|
|
55
|
+
/** Base font size as a CSS length, e.g. `16px`. */
|
|
56
|
+
baseSize: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Button/control shape on the checkout. */
|
|
60
|
+
export interface CheckoutStyleButton {
|
|
61
|
+
/** Corner radius as a CSS length, e.g. `6px` (`0` for square). */
|
|
62
|
+
radius: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The structured checkout-styling descriptor — the `checkout-style.json`
|
|
67
|
+
* artifact. This is the model a merchant edits (U12) and that U11 renders into
|
|
68
|
+
* the CSS pushed to Foxy. A published artifact is a `DeepPartial` of this
|
|
69
|
+
* (a merchant may set one field without restating the rest); `resolveCheckout
|
|
70
|
+
* Style` merges it over `DEFAULT_CHECKOUT_STYLE` so the resolved value is always
|
|
71
|
+
* complete.
|
|
72
|
+
*/
|
|
73
|
+
export interface CheckoutStyle {
|
|
74
|
+
colors: CheckoutStyleColors;
|
|
75
|
+
typography: CheckoutStyleTypography;
|
|
76
|
+
button: CheckoutStyleButton;
|
|
77
|
+
/** Optional logo URL shown on the checkout header (absolute https URL). */
|
|
78
|
+
logoUrl?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Neutral, accessible baseline used when a tenant publishes nothing (or omits a
|
|
83
|
+
* field). Chosen for AA contrast on the default pairs so an un-styled checkout
|
|
84
|
+
* is never broken — a merchant only overrides what they want to change.
|
|
85
|
+
*/
|
|
86
|
+
export const DEFAULT_CHECKOUT_STYLE: CheckoutStyle = {
|
|
87
|
+
colors: {
|
|
88
|
+
background: "#ffffff",
|
|
89
|
+
surface: "#f7f7f8",
|
|
90
|
+
primary: "#111111",
|
|
91
|
+
primaryContrast: "#ffffff",
|
|
92
|
+
accent: "#2563eb",
|
|
93
|
+
text: "#1a1a1a",
|
|
94
|
+
},
|
|
95
|
+
typography: {
|
|
96
|
+
fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
|
|
97
|
+
baseSize: "16px",
|
|
98
|
+
},
|
|
99
|
+
button: {
|
|
100
|
+
radius: "6px",
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Derive a checkout style FROM a tenant's brand `ThemeTokens`, so the checkout is
|
|
106
|
+
* consistent with the look/feel the merchant already chose for the storefront
|
|
107
|
+
* instead of a neutral default. This is the "from theme" seam: the settings editor
|
|
108
|
+
* (U12) seeds a merchant's starting style from it, and a tenant that ships a brand
|
|
109
|
+
* theme gets an in-brand checkout with no hand-authoring.
|
|
110
|
+
*
|
|
111
|
+
* It maps the small, opinionated checkout surface onto the full theme palette:
|
|
112
|
+
* - colors ← `color.{bg,surface,primary,primary-contrast,accent,text}`
|
|
113
|
+
* - typography.fontFamily ← the theme's BODY family, expanded into a web-safe
|
|
114
|
+
* stack (the checkout is body copy + a CTA, not display type)
|
|
115
|
+
* - button.radius ← `shape.radius.md` (the standard control radius)
|
|
116
|
+
* - logoUrl ← `brand.logoLight`, but ONLY when it is an absolute https URL —
|
|
117
|
+
* Foxy fetches the checkout server-side, so a repo-relative logo path
|
|
118
|
+
* (e.g. `/brand/logo.svg`) can't resolve there and is dropped.
|
|
119
|
+
*
|
|
120
|
+
* Input is a `DeepPartial` (a tenant's `theme.json` override is sparse), so every
|
|
121
|
+
* field falls back to `DEFAULT_CHECKOUT_STYLE` when the theme omits it — the result
|
|
122
|
+
* is always a complete `CheckoutStyle`. ASCII discipline (Foxy's cache is not
|
|
123
|
+
* Unicode-safe): any derived string leaf that isn't `isAsciiSafe` is dropped in
|
|
124
|
+
* favor of the default, so a theme can never produce a checkout-blanking value.
|
|
125
|
+
* Pure + node-free.
|
|
126
|
+
*/
|
|
127
|
+
export function checkoutStyleFromTheme(
|
|
128
|
+
theme: DeepPartial<ThemeTokens> | null | undefined,
|
|
129
|
+
): CheckoutStyle {
|
|
130
|
+
const d = DEFAULT_CHECKOUT_STYLE;
|
|
131
|
+
// Take a theme string only when it is a non-empty, ASCII-safe scalar; otherwise
|
|
132
|
+
// fall back so a missing/exotic token never yields a bad checkout value.
|
|
133
|
+
const take = (value: unknown, fallback: string): string =>
|
|
134
|
+
typeof value === "string" && value.trim() && isAsciiSafe(value)
|
|
135
|
+
? value
|
|
136
|
+
: fallback;
|
|
137
|
+
|
|
138
|
+
const color = theme?.color;
|
|
139
|
+
const shape = theme?.shape;
|
|
140
|
+
const bodyFamily = theme?.type?.body?.family;
|
|
141
|
+
const logo = theme?.brand?.logoLight;
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
colors: {
|
|
145
|
+
background: take(color?.bg, d.colors.background),
|
|
146
|
+
surface: take(color?.surface, d.colors.surface),
|
|
147
|
+
primary: take(color?.primary, d.colors.primary),
|
|
148
|
+
primaryContrast: take(color?.["primary-contrast"], d.colors.primaryContrast),
|
|
149
|
+
accent: take(color?.accent, d.colors.accent),
|
|
150
|
+
text: take(color?.text, d.colors.text),
|
|
151
|
+
},
|
|
152
|
+
typography: {
|
|
153
|
+
fontFamily: fontStackFromFamily(bodyFamily, d.typography.fontFamily),
|
|
154
|
+
baseSize: d.typography.baseSize,
|
|
155
|
+
},
|
|
156
|
+
button: {
|
|
157
|
+
radius: take(shape?.radius?.md, d.button.radius),
|
|
158
|
+
},
|
|
159
|
+
...(typeof logo === "string" &&
|
|
160
|
+
isAsciiSafe(logo) &&
|
|
161
|
+
/^https:\/\/\S+$/.test(logo.trim())
|
|
162
|
+
? { logoUrl: logo.trim() }
|
|
163
|
+
: {}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Expand a single theme font FAMILY (`"Inter"`) into a full CSS font-family stack
|
|
169
|
+
* with web-safe fallbacks — the checkout must render legibly even where the brand
|
|
170
|
+
* webfont isn't loaded. A value that already looks like a stack (has a comma) is
|
|
171
|
+
* used as-is. Falls back to `fallback` when the family is absent or non-ASCII.
|
|
172
|
+
*/
|
|
173
|
+
function fontStackFromFamily(family: unknown, fallback: string): string {
|
|
174
|
+
if (typeof family !== "string" || !family.trim() || !isAsciiSafe(family)) {
|
|
175
|
+
return fallback;
|
|
176
|
+
}
|
|
177
|
+
const f = family.trim();
|
|
178
|
+
if (f.includes(",")) return f;
|
|
179
|
+
const quoted = /\s/.test(f) ? `"${f}"` : f;
|
|
180
|
+
return `${quoted}, system-ui, -apple-system, Segoe UI, Roboto, sans-serif`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Canonical published-artifact path for a tenant's structured checkout style.
|
|
185
|
+
* Same colocated `tenants/<id>/…` contract as theme/capabilities, so the
|
|
186
|
+
* reconcile pipeline keys the KV artifact by it and the storefront reads it back
|
|
187
|
+
* per environment.
|
|
188
|
+
*/
|
|
189
|
+
export function checkoutStyleArtifactPath(tenantId: string): string {
|
|
190
|
+
return `tenants/${tenantId}/checkout-style.json`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Canonical path for the optional raw-CSS override artifact — the escape hatch a
|
|
195
|
+
* merchant edits directly (or that advanced templates ship). Sibling to the
|
|
196
|
+
* structured descriptor; hosted + pushed to Foxy verbatim by U11.
|
|
197
|
+
*/
|
|
198
|
+
export function checkoutStyleCssPath(tenantId: string): string {
|
|
199
|
+
return `tenants/${tenantId}/checkout.css`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Safely parse a published `checkout-style.json` string into a partial override.
|
|
204
|
+
* Never throws — returns null on malformed JSON or a non-object top level, so the
|
|
205
|
+
* caller falls back to the default (mirrors the theme read path's tolerance).
|
|
206
|
+
*/
|
|
207
|
+
export function parseCheckoutStyle(
|
|
208
|
+
raw: string | null,
|
|
209
|
+
): DeepPartial<CheckoutStyle> | null {
|
|
210
|
+
if (!raw) return null;
|
|
211
|
+
try {
|
|
212
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
213
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return parsed as DeepPartial<CheckoutStyle>;
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Deep-merge a partial override over a complete base, so a merchant can set one
|
|
224
|
+
* field without restating the rest. Objects merge recursively; scalars replace.
|
|
225
|
+
* Node-free and self-contained (mirrors `mergeTheme`'s deepMerge).
|
|
226
|
+
*/
|
|
227
|
+
export function mergeCheckoutStyle(
|
|
228
|
+
base: CheckoutStyle,
|
|
229
|
+
override?: DeepPartial<CheckoutStyle> | null,
|
|
230
|
+
): CheckoutStyle {
|
|
231
|
+
if (!override) return base;
|
|
232
|
+
return deepMerge(base, override) as CheckoutStyle;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function deepMerge<T>(base: T, over: DeepPartial<T>): T {
|
|
236
|
+
if (typeof base === "object" && base !== null && !Array.isArray(base)) {
|
|
237
|
+
const out: Record<string, unknown> = { ...(base as Record<string, unknown>) };
|
|
238
|
+
for (const [k, v] of Object.entries(over as Record<string, unknown>)) {
|
|
239
|
+
if (v === undefined) continue;
|
|
240
|
+
const bv = (base as Record<string, unknown>)[k];
|
|
241
|
+
out[k] =
|
|
242
|
+
bv && typeof bv === "object" && !Array.isArray(bv) && typeof v === "object"
|
|
243
|
+
? deepMerge(bv, v as DeepPartial<unknown>)
|
|
244
|
+
: v;
|
|
245
|
+
}
|
|
246
|
+
return out as T;
|
|
247
|
+
}
|
|
248
|
+
return (over as unknown as T) ?? base;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Resolve a tenant's effective checkout style for an environment: the
|
|
253
|
+
* `DEFAULT_CHECKOUT_STYLE` baseline with any published `checkout-style.json`
|
|
254
|
+
* override merged on top. Reads through the versioned publish spine (channel →
|
|
255
|
+
* version → artifact) via `readPublishedArtifact`; a malformed/absent artifact
|
|
256
|
+
* degrades to the default. Never throws.
|
|
257
|
+
*/
|
|
258
|
+
export async function resolveCheckoutStyle(
|
|
259
|
+
get: KvGet,
|
|
260
|
+
tenantId: string,
|
|
261
|
+
env: CustomizationEnv,
|
|
262
|
+
): Promise<CheckoutStyle> {
|
|
263
|
+
const raw = await readPublishedArtifact(
|
|
264
|
+
get,
|
|
265
|
+
tenantId,
|
|
266
|
+
env,
|
|
267
|
+
checkoutStyleArtifactPath(tenantId),
|
|
268
|
+
);
|
|
269
|
+
return mergeCheckoutStyle(DEFAULT_CHECKOUT_STYLE, parseCheckoutStyle(raw));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Resolve a tenant's published raw checkout CSS override for an environment, or
|
|
274
|
+
* null when none is published (so U11 pushes only the generated tokens). Reads
|
|
275
|
+
* the sibling `checkout.css` artifact through the same versioned spine. Never
|
|
276
|
+
* throws.
|
|
277
|
+
*/
|
|
278
|
+
export async function resolveCheckoutStyleCss(
|
|
279
|
+
get: KvGet,
|
|
280
|
+
tenantId: string,
|
|
281
|
+
env: CustomizationEnv,
|
|
282
|
+
): Promise<string | null> {
|
|
283
|
+
return readPublishedArtifact(
|
|
284
|
+
get,
|
|
285
|
+
tenantId,
|
|
286
|
+
env,
|
|
287
|
+
checkoutStyleCssPath(tenantId),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// CSS serialization (the artifact pushed to FoxyCart) — U11
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* True iff every character is 7-bit ASCII (code point < 128). FoxyCart's
|
|
297
|
+
* template cache pass is NOT Unicode-safe — a non-ASCII byte in the cached
|
|
298
|
+
* checkout content yields a BLANK page. So the publisher (U11) MUST gate both
|
|
299
|
+
* the generated token CSS and any raw override through this before pushing a
|
|
300
|
+
* `content_url` to `/theme/foxy`. We reject rather than transliterate, so a
|
|
301
|
+
* merchant is told to fix their input instead of getting silently-mangled CSS.
|
|
302
|
+
*/
|
|
303
|
+
export function isAsciiSafe(s: string): boolean {
|
|
304
|
+
for (let i = 0; i < s.length; i++) {
|
|
305
|
+
if (s.charCodeAt(i) > 127) return false;
|
|
306
|
+
}
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** CSS custom-property name for each checkout-style color token. */
|
|
311
|
+
const CHECKOUT_COLOR_VARS: Record<keyof CheckoutStyleColors, string> = {
|
|
312
|
+
background: "--tot-checkout-bg",
|
|
313
|
+
surface: "--tot-checkout-surface",
|
|
314
|
+
primary: "--tot-checkout-primary",
|
|
315
|
+
primaryContrast: "--tot-checkout-primary-contrast",
|
|
316
|
+
accent: "--tot-checkout-accent",
|
|
317
|
+
text: "--tot-checkout-text",
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Serialize a resolved `CheckoutStyle` into a `:root { … }` block of CSS custom
|
|
322
|
+
* properties (`--tot-checkout-*`) that a FoxyCart checkout template references.
|
|
323
|
+
* Deterministic (stable key order) so an unchanged style produces byte-identical
|
|
324
|
+
* output — the publisher can hash it to skip no-op pushes. The `logoUrl`, when
|
|
325
|
+
* set, is emitted as `--tot-checkout-logo: url("…")`.
|
|
326
|
+
*
|
|
327
|
+
* Output is ASCII by construction for the default palette; a merchant-supplied
|
|
328
|
+
* value (e.g. an exotic font-family or logo URL) could introduce non-ASCII, so
|
|
329
|
+
* the publisher still validates the final string with `isAsciiSafe` — this
|
|
330
|
+
* function does not throw, it just serializes.
|
|
331
|
+
*/
|
|
332
|
+
export function checkoutStyleToCss(style: CheckoutStyle): string {
|
|
333
|
+
const lines: string[] = [];
|
|
334
|
+
const push = (name: string, value: string) => lines.push(` ${name}: ${value};`);
|
|
335
|
+
|
|
336
|
+
for (const key of Object.keys(CHECKOUT_COLOR_VARS) as (keyof CheckoutStyleColors)[]) {
|
|
337
|
+
push(CHECKOUT_COLOR_VARS[key], style.colors[key]);
|
|
338
|
+
}
|
|
339
|
+
push("--tot-checkout-font-family", style.typography.fontFamily);
|
|
340
|
+
push("--tot-checkout-font-size", style.typography.baseSize);
|
|
341
|
+
push("--tot-checkout-button-radius", style.button.radius);
|
|
342
|
+
if (style.logoUrl) {
|
|
343
|
+
push("--tot-checkout-logo", `url("${style.logoUrl}")`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return `:root {\n${lines.join("\n")}\n}`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Assemble the full checkout CSS served at the stable storefront URL that becomes
|
|
351
|
+
* FoxyCart's `content_url`: the generated token block (`checkoutStyleToCss` over
|
|
352
|
+
* the resolved `CheckoutStyle`) followed by the tenant's optional raw-CSS override.
|
|
353
|
+
*
|
|
354
|
+
* ASCII discipline (Foxy's cache pass is not Unicode-safe → blank checkout): the
|
|
355
|
+
* raw override is appended ONLY when it is `isAsciiSafe` — a non-ASCII override is
|
|
356
|
+
* DROPPED here so a bad byte can never blank the live checkout (the authoritative
|
|
357
|
+
* reject-at-publish gate is U13's promote GateReport; this is defense-in-depth at
|
|
358
|
+
* serve time). The token block is ASCII for the default palette and any sane
|
|
359
|
+
* merchant value. Reads both artifacts per env through the versioned spine; never
|
|
360
|
+
* throws.
|
|
361
|
+
*/
|
|
362
|
+
export async function buildCheckoutStyleCss(
|
|
363
|
+
get: KvGet,
|
|
364
|
+
tenantId: string,
|
|
365
|
+
env: CustomizationEnv,
|
|
366
|
+
): Promise<string> {
|
|
367
|
+
const tokenCss = checkoutStyleToCss(
|
|
368
|
+
await resolveCheckoutStyle(get, tenantId, env),
|
|
369
|
+
);
|
|
370
|
+
const raw = await resolveCheckoutStyleCss(get, tenantId, env);
|
|
371
|
+
if (raw && isAsciiSafe(raw)) {
|
|
372
|
+
return `${tokenCss}\n\n${raw}`;
|
|
373
|
+
}
|
|
374
|
+
return tokenCss;
|
|
375
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-Security-Policy builder. Pure + dep-free so it's unit-testable and
|
|
3
|
+
* reusable across the storefront.
|
|
4
|
+
*
|
|
5
|
+
* Produces a strict policy: `default-src 'self'`, no `unsafe-inline`. The
|
|
6
|
+
* platform's own inline `<style>`/`<script is:inline>` are allowed via a
|
|
7
|
+
* per-request nonce; everything else must be `'self'`, an explicitly allowlisted
|
|
8
|
+
* host, or (for tenant scripts) a per-version hash passed in `scriptSrc`.
|
|
9
|
+
*
|
|
10
|
+
* The middleware decides the header name — `Content-Security-Policy` (enforce)
|
|
11
|
+
* vs `Content-Security-Policy-Report-Only` (rollout). This only builds the value.
|
|
12
|
+
*/
|
|
13
|
+
export interface CspOptions {
|
|
14
|
+
/** Per-request nonce; the same value goes on the platform's inline tags. */
|
|
15
|
+
nonce: string;
|
|
16
|
+
/** Extra script-src entries: allowlisted CDNs + per-version tenant hashes. */
|
|
17
|
+
scriptSrc?: readonly string[];
|
|
18
|
+
styleSrc?: readonly string[];
|
|
19
|
+
fontSrc?: readonly string[];
|
|
20
|
+
imgSrc?: readonly string[];
|
|
21
|
+
connectSrc?: readonly string[];
|
|
22
|
+
/** Sandboxed-widget origins. */
|
|
23
|
+
frameSrc?: readonly string[];
|
|
24
|
+
/**
|
|
25
|
+
* Extra `form-action` targets. Default is `'self'`; the FoxyCart add-to-cart
|
|
26
|
+
* form POSTs cross-origin to the store's `/cart`, so that origin is added here.
|
|
27
|
+
*/
|
|
28
|
+
formAction?: readonly string[];
|
|
29
|
+
/** Reporting group name for `report-to` (paired with a Reporting-Endpoints header). */
|
|
30
|
+
reportGroup?: string;
|
|
31
|
+
/** Legacy `report-uri` path (broader browser support during rollout). */
|
|
32
|
+
reportUri?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildCsp(opts: CspOptions): string {
|
|
36
|
+
const nonce = `'nonce-${opts.nonce}'`;
|
|
37
|
+
const directives: Record<string, string[]> = {
|
|
38
|
+
"default-src": ["'self'"],
|
|
39
|
+
"base-uri": ["'self'"],
|
|
40
|
+
"object-src": ["'none'"],
|
|
41
|
+
"frame-ancestors": ["'self'"],
|
|
42
|
+
"form-action": ["'self'", ...(opts.formAction ?? [])],
|
|
43
|
+
"script-src": ["'self'", nonce, ...(opts.scriptSrc ?? [])],
|
|
44
|
+
"style-src": ["'self'", nonce, ...(opts.styleSrc ?? [])],
|
|
45
|
+
// Inline style ATTRIBUTES (style="…") can't carry a nonce; CSS can't execute
|
|
46
|
+
// JS, so this is the one tolerated relaxation. style-src-elem still inherits
|
|
47
|
+
// the strict 'self'+nonce from style-src; script-src stays fully strict.
|
|
48
|
+
"style-src-attr": ["'unsafe-inline'"],
|
|
49
|
+
"font-src": ["'self'", ...(opts.fontSrc ?? [])],
|
|
50
|
+
"img-src": ["'self'", "data:", ...(opts.imgSrc ?? [])],
|
|
51
|
+
"connect-src": ["'self'", ...(opts.connectSrc ?? [])],
|
|
52
|
+
"frame-src": ["'self'", ...(opts.frameSrc ?? [])],
|
|
53
|
+
};
|
|
54
|
+
const parts = Object.entries(directives).map(
|
|
55
|
+
([name, values]) => `${name} ${values.join(" ")}`,
|
|
56
|
+
);
|
|
57
|
+
if (opts.reportUri) parts.push(`report-uri ${opts.reportUri}`);
|
|
58
|
+
if (opts.reportGroup) parts.push(`report-to ${opts.reportGroup}`);
|
|
59
|
+
return parts.join("; ");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Endpoint that collects CSP violation reports during the report-only rollout. */
|
|
63
|
+
export const CSP_REPORT_PATH = "/api/csp-report";
|
|
64
|
+
export const CSP_REPORT_GROUP = "csp-endpoint";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The storefront's known-good external origins. The middleware passes these to
|
|
68
|
+
* `buildCsp` and may extend `scriptSrc` (tenant hashes) + `frameSrc` (the widget
|
|
69
|
+
* origin) per request.
|
|
70
|
+
*/
|
|
71
|
+
export const STOREFRONT_CSP_ALLOWLIST = {
|
|
72
|
+
scriptSrc: ["https://static.cloudflareinsights.com"],
|
|
73
|
+
styleSrc: ["https://fonts.googleapis.com"],
|
|
74
|
+
fontSrc: ["https://fonts.gstatic.com"],
|
|
75
|
+
// Product imagery is rehosted to R2 or served from remote CDNs.
|
|
76
|
+
imgSrc: ["https:"],
|
|
77
|
+
connectSrc: ["https://cloudflareinsights.com"],
|
|
78
|
+
} as const;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* CSP additions for a FoxyCart-enabled tenant. The loader.js comes from
|
|
82
|
+
* `cdn.foxycart.com`; the sidecart XHR + iframe + the add-to-cart form POST all
|
|
83
|
+
* hit the store's own origin (`<store>.foxycart.com` or a branded checkout
|
|
84
|
+
* domain). Merge these into `buildCsp` per request when `locals.commerce.foxy`
|
|
85
|
+
* is present so the strict policy still passes.
|
|
86
|
+
*/
|
|
87
|
+
export function foxyCspAdditions(storeOrigin: string): {
|
|
88
|
+
scriptSrc: string[];
|
|
89
|
+
styleSrc: string[];
|
|
90
|
+
fontSrc: string[];
|
|
91
|
+
connectSrc: string[];
|
|
92
|
+
frameSrc: string[];
|
|
93
|
+
formAction: string[];
|
|
94
|
+
imgSrc: string[];
|
|
95
|
+
} {
|
|
96
|
+
const cdn = "https://cdn.foxycart.com";
|
|
97
|
+
// Foxy's loader pulls its own JS, CSS, and fonts from the CDN and renders the
|
|
98
|
+
// sidecart/checkout from the store origin — so it needs script/style/font from
|
|
99
|
+
// the CDN and connect/frame/form to the store.
|
|
100
|
+
return {
|
|
101
|
+
scriptSrc: [cdn, storeOrigin],
|
|
102
|
+
styleSrc: [cdn, storeOrigin],
|
|
103
|
+
fontSrc: [cdn, storeOrigin],
|
|
104
|
+
connectSrc: [cdn, storeOrigin],
|
|
105
|
+
frameSrc: [cdn, storeOrigin],
|
|
106
|
+
formAction: [storeOrigin],
|
|
107
|
+
imgSrc: [cdn, storeOrigin],
|
|
108
|
+
};
|
|
109
|
+
}
|