@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,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gated preview support: signing preview access and resolving which
|
|
3
|
+
* customization environment a request sees.
|
|
4
|
+
*
|
|
5
|
+
* Two pure, runtime-portable pieces (Web Crypto + btoa/atob, no `node:*`):
|
|
6
|
+
*
|
|
7
|
+
* 1. signed, short-TTL **preview tokens** (HMAC-SHA256) that grant access to a
|
|
8
|
+
* tenant's `test` customization on a preview host; and
|
|
9
|
+
* 2. `resolveCustomizationEnv` — the live-vs-test decision the middleware makes
|
|
10
|
+
* per request.
|
|
11
|
+
*
|
|
12
|
+
* Security properties this enforces:
|
|
13
|
+
* - **Production hosts never serve `test`.** A token is only consulted on a
|
|
14
|
+
* preview host, so a leaked token can't flip a live storefront.
|
|
15
|
+
* - **Tokens are tenant-bound.** A token minted for tenant A cannot preview
|
|
16
|
+
* tenant B (no cross-tenant leakage).
|
|
17
|
+
* - **Tokens expire.** `exp` is checked against the caller-supplied clock.
|
|
18
|
+
* - **Test is never indexable.** The resolution carries `noindex: true` so the
|
|
19
|
+
* middleware can set `X-Robots-Tag: noindex`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Claims carried by a preview token. */
|
|
23
|
+
export interface PreviewClaims {
|
|
24
|
+
tenantId: string;
|
|
25
|
+
env: "test";
|
|
26
|
+
/** Expiry, epoch seconds. */
|
|
27
|
+
exp: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const textEncoder = new TextEncoder();
|
|
31
|
+
|
|
32
|
+
function bytesToB64Url(bytes: Uint8Array): string {
|
|
33
|
+
let bin = "";
|
|
34
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
35
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function b64UrlToBytes(s: string): Uint8Array {
|
|
39
|
+
const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - (s.length % 4));
|
|
40
|
+
const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + pad);
|
|
41
|
+
const out = new Uint8Array(bin.length);
|
|
42
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function hmacKey(secret: string): Promise<CryptoKey> {
|
|
47
|
+
return crypto.subtle.importKey(
|
|
48
|
+
"raw",
|
|
49
|
+
textEncoder.encode(secret) as unknown as BufferSource,
|
|
50
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
51
|
+
false,
|
|
52
|
+
["sign", "verify"],
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Mint a preview token: `<payloadB64Url>.<sigB64Url>`. `ttlSeconds` is added to
|
|
58
|
+
* `nowSeconds` (caller supplies the clock so this stays pure/testable).
|
|
59
|
+
*/
|
|
60
|
+
export async function signPreviewToken(
|
|
61
|
+
secret: string,
|
|
62
|
+
tenantId: string,
|
|
63
|
+
ttlSeconds: number,
|
|
64
|
+
nowSeconds: number,
|
|
65
|
+
): Promise<string> {
|
|
66
|
+
const claims: PreviewClaims = {
|
|
67
|
+
tenantId,
|
|
68
|
+
env: "test",
|
|
69
|
+
exp: nowSeconds + ttlSeconds,
|
|
70
|
+
};
|
|
71
|
+
const payload = bytesToB64Url(textEncoder.encode(JSON.stringify(claims)));
|
|
72
|
+
const key = await hmacKey(secret);
|
|
73
|
+
const sig = await crypto.subtle.sign(
|
|
74
|
+
"HMAC",
|
|
75
|
+
key,
|
|
76
|
+
textEncoder.encode(payload) as unknown as BufferSource,
|
|
77
|
+
);
|
|
78
|
+
return `${payload}.${bytesToB64Url(new Uint8Array(sig))}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type VerifyResult =
|
|
82
|
+
| { ok: true; claims: PreviewClaims }
|
|
83
|
+
| { ok: false; reason: "malformed" | "bad-signature" | "expired" };
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Verify a preview token against `secret` and the caller's clock. Signature is
|
|
87
|
+
* checked with `crypto.subtle.verify` (constant-time) before expiry.
|
|
88
|
+
*/
|
|
89
|
+
export async function verifyPreviewToken(
|
|
90
|
+
secret: string,
|
|
91
|
+
token: string,
|
|
92
|
+
nowSeconds: number,
|
|
93
|
+
): Promise<VerifyResult> {
|
|
94
|
+
const dot = token.indexOf(".");
|
|
95
|
+
if (dot <= 0 || dot === token.length - 1) return { ok: false, reason: "malformed" };
|
|
96
|
+
const payload = token.slice(0, dot);
|
|
97
|
+
const sigPart = token.slice(dot + 1);
|
|
98
|
+
|
|
99
|
+
let sig: Uint8Array;
|
|
100
|
+
try {
|
|
101
|
+
sig = b64UrlToBytes(sigPart);
|
|
102
|
+
} catch {
|
|
103
|
+
return { ok: false, reason: "malformed" };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const key = await hmacKey(secret);
|
|
107
|
+
const valid = await crypto.subtle.verify(
|
|
108
|
+
"HMAC",
|
|
109
|
+
key,
|
|
110
|
+
sig as unknown as BufferSource,
|
|
111
|
+
textEncoder.encode(payload) as unknown as BufferSource,
|
|
112
|
+
);
|
|
113
|
+
if (!valid) return { ok: false, reason: "bad-signature" };
|
|
114
|
+
|
|
115
|
+
let claims: PreviewClaims;
|
|
116
|
+
try {
|
|
117
|
+
claims = JSON.parse(new TextDecoder().decode(b64UrlToBytes(payload)));
|
|
118
|
+
} catch {
|
|
119
|
+
return { ok: false, reason: "malformed" };
|
|
120
|
+
}
|
|
121
|
+
if (
|
|
122
|
+
!claims ||
|
|
123
|
+
claims.env !== "test" ||
|
|
124
|
+
typeof claims.tenantId !== "string" ||
|
|
125
|
+
typeof claims.exp !== "number"
|
|
126
|
+
) {
|
|
127
|
+
return { ok: false, reason: "malformed" };
|
|
128
|
+
}
|
|
129
|
+
if (claims.exp <= nowSeconds) return { ok: false, reason: "expired" };
|
|
130
|
+
return { ok: true, claims };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type CustomizationEnv = "live" | "test";
|
|
134
|
+
|
|
135
|
+
export interface EnvResolutionInput {
|
|
136
|
+
/** True when the request arrived on a preview host (e.g. `preview.*`). */
|
|
137
|
+
isPreviewHost: boolean;
|
|
138
|
+
/** The tenant resolved for this request. */
|
|
139
|
+
tenantId: string;
|
|
140
|
+
/** Token from `?__preview=` or the `tot_preview` cookie, if any. */
|
|
141
|
+
token?: string | null;
|
|
142
|
+
/** Platform preview-signing secret (env), if configured. */
|
|
143
|
+
secret?: string | null;
|
|
144
|
+
nowSeconds: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface EnvResolution {
|
|
148
|
+
env: CustomizationEnv;
|
|
149
|
+
/** Set `X-Robots-Tag: noindex` when true (test is never indexable). */
|
|
150
|
+
noindex: boolean;
|
|
151
|
+
/** When set, the middleware should short-circuit with this status. */
|
|
152
|
+
reject?: { status: number; message: string };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Decide which customization environment to serve. Production hosts always get
|
|
157
|
+
* `live` and never consult a token. Preview hosts are auth-gated: a valid,
|
|
158
|
+
* unexpired, tenant-matching token yields `test` (noindex); anything else is
|
|
159
|
+
* rejected.
|
|
160
|
+
*/
|
|
161
|
+
export async function resolveCustomizationEnv(
|
|
162
|
+
input: EnvResolutionInput,
|
|
163
|
+
): Promise<EnvResolution> {
|
|
164
|
+
if (!input.isPreviewHost) {
|
|
165
|
+
return { env: "live", noindex: false };
|
|
166
|
+
}
|
|
167
|
+
if (!input.secret) {
|
|
168
|
+
return {
|
|
169
|
+
env: "live",
|
|
170
|
+
noindex: true,
|
|
171
|
+
reject: { status: 503, message: "Preview is not configured." },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (!input.token) {
|
|
175
|
+
return {
|
|
176
|
+
env: "live",
|
|
177
|
+
noindex: true,
|
|
178
|
+
reject: { status: 401, message: "Preview token required." },
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const result = await verifyPreviewToken(input.secret, input.token, input.nowSeconds);
|
|
182
|
+
if (!result.ok) {
|
|
183
|
+
return {
|
|
184
|
+
env: "live",
|
|
185
|
+
noindex: true,
|
|
186
|
+
reject: { status: 401, message: `Invalid preview token (${result.reason}).` },
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (result.claims.tenantId !== input.tenantId) {
|
|
190
|
+
return {
|
|
191
|
+
env: "live",
|
|
192
|
+
noindex: true,
|
|
193
|
+
reject: { status: 403, message: "Preview token is for a different tenant." },
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return { env: "test", noindex: true };
|
|
197
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the tenant preview model: the `.tot/config.json` mapping shape and
|
|
3
|
+
* the preview-state record — which commit a tenant's preview targets vs. what is
|
|
4
|
+
* currently rendered — plus its KV key. Read-only shapes; no logic here.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Repo-relative path of the tenant repo's mapping config. */
|
|
8
|
+
export const TOT_CONFIG_PATH = ".tot/config.json";
|
|
9
|
+
|
|
10
|
+
/** One mapping from a tenant-repo prefix to a storefront artifact path. */
|
|
11
|
+
export interface TotRepoMapping {
|
|
12
|
+
/** Tenant-repo path/prefix (e.g. "theme.json", "content/", "public/"). */
|
|
13
|
+
workspace: string;
|
|
14
|
+
/** Storefront artifact path/prefix it maps to. */
|
|
15
|
+
repo: string;
|
|
16
|
+
kind: "file" | "tree";
|
|
17
|
+
}
|
|
18
|
+
export interface TotRepoConfig {
|
|
19
|
+
tenant: string;
|
|
20
|
+
scope?: string;
|
|
21
|
+
mappings: TotRepoMapping[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** One compliance check row (mirrors @tot/private-controlplane's EvidenceCheck —
|
|
25
|
+
* kept structural here so this read-only type stays dependency-free). */
|
|
26
|
+
export interface PreviewStateEvidenceCheck {
|
|
27
|
+
id: string;
|
|
28
|
+
label: string;
|
|
29
|
+
status: string;
|
|
30
|
+
detail?: string;
|
|
31
|
+
/** A concrete fix suggestion for a non-pass check (evidence producer's own —
|
|
32
|
+
* the same "why" + "fix" the CLI's `tot submit --watch` prints). */
|
|
33
|
+
hint?: string;
|
|
34
|
+
/** Whether this check is ship-blocking (REQUIRED_FOR_PROMOTE). Computed at
|
|
35
|
+
* merge time (preview-state-evidence.ts), not stored by the producer. */
|
|
36
|
+
required?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Fail-safe preview state — target/actual can diverge when target is broken. */
|
|
40
|
+
export interface PreviewState {
|
|
41
|
+
tenantId: string;
|
|
42
|
+
/** The commit the dev wants previewed. */
|
|
43
|
+
target: string | null;
|
|
44
|
+
/** The commit currently rendered (last successfully materialized). */
|
|
45
|
+
actual: string | null;
|
|
46
|
+
/** Why target != actual, when the target failed to render. Null on success. */
|
|
47
|
+
lastError: string | null;
|
|
48
|
+
lastReconcileAt: string | null;
|
|
49
|
+
/**
|
|
50
|
+
* The compliance verdict for the last successful reconcile (ADR 0010's evidence,
|
|
51
|
+
* mirrored here so the PUBLIC, unauthenticated /preview/dashboard page can show
|
|
52
|
+
* it — a shareable link should be as informative as the CLI's, without an MCP
|
|
53
|
+
* round trip). Optional/absent on a record `reconcilePreview` itself wrote
|
|
54
|
+
* (that pipeline doesn't know about evidence) — only the reconcile ROUTE's
|
|
55
|
+
* post-hoc merge sets it, after `reportPreviewEvidence` computes it. Absent and
|
|
56
|
+
* `null` both mean "no verdict yet"; callers should treat them the same.
|
|
57
|
+
*/
|
|
58
|
+
evidence?: { checks: PreviewStateEvidenceCheck[]; promotable: boolean; generatedAt: string } | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function previewStateKey(tenantId: string): string {
|
|
62
|
+
return `cust:preview-state:${tenantId}`;
|
|
63
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime read path for published customizations (store→app wiring). Resolves,
|
|
3
|
+
* for a tenant + environment, the content of a published artifact from a simple
|
|
4
|
+
* key/value store — so the `{ live, test }` pointer actually selects what the
|
|
5
|
+
* storefront serves (the live domain → `live`, a gated preview → `test`).
|
|
6
|
+
*
|
|
7
|
+
* Pure + dependency-free: the caller injects a `get(key) => Promise<string|null>`
|
|
8
|
+
* (Cloudflare KV in prod, MemoryKv in dev), keeping this module storage-agnostic
|
|
9
|
+
* and unit-testable. The app layer parses the returned string into the right
|
|
10
|
+
* shape (e.g. DeepPartial<ThemeTokens>) and falls back to the build-time artifact
|
|
11
|
+
* when nothing is published (so an un-customized tenant is unaffected).
|
|
12
|
+
*/
|
|
13
|
+
import {
|
|
14
|
+
channelFromEnv,
|
|
15
|
+
type ChannelName,
|
|
16
|
+
type PointerRecord,
|
|
17
|
+
type SiteVersionManifest,
|
|
18
|
+
} from "./customization-versioning.js";
|
|
19
|
+
import type { CustomizationEnv } from "./customization-preview.js";
|
|
20
|
+
|
|
21
|
+
export type KvGet = (key: string) => Promise<string | null>;
|
|
22
|
+
|
|
23
|
+
/** KV key layout for the customization store (mirrors the tenant-cache style). */
|
|
24
|
+
export function customizationPointerKey(tenantId: string): string {
|
|
25
|
+
return `cust:pointer:${tenantId}`;
|
|
26
|
+
}
|
|
27
|
+
export function customizationArtifactKey(
|
|
28
|
+
tenantId: string,
|
|
29
|
+
versionId: string,
|
|
30
|
+
): string {
|
|
31
|
+
return `cust:artifact:${tenantId}:${versionId}`;
|
|
32
|
+
}
|
|
33
|
+
/** KV key for one immutable site-version manifest. */
|
|
34
|
+
export function siteVersionKey(tenantId: string, versionId: string): string {
|
|
35
|
+
return `cust:version:${tenantId}:${versionId}`;
|
|
36
|
+
}
|
|
37
|
+
/** KV key for a tenant's version-history index (list of summaries). */
|
|
38
|
+
export function siteVersionIndexKey(tenantId: string): string {
|
|
39
|
+
return `cust:versions:${tenantId}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Canonical artifact path for a tenant's theme override.
|
|
44
|
+
*
|
|
45
|
+
* Colocated layout: a tenant IS the repo-root directory `tenants/<id>/`, so the
|
|
46
|
+
* theme lives at `tenants/<id>/theme.json`. The storefront's published-content
|
|
47
|
+
* read overlay (provider.ts) keys KV artifacts by this path.
|
|
48
|
+
*/
|
|
49
|
+
export function themeArtifactPath(tenantId: string): string {
|
|
50
|
+
return `tenants/${tenantId}/theme.json`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Canonical artifact path for a tenant content file (e.g. "home.json"). Colocated
|
|
55
|
+
* under `tenants/<id>/content/` — see themeArtifactPath for the contract note.
|
|
56
|
+
*/
|
|
57
|
+
export function contentArtifactPath(tenantId: string, relative: string): string {
|
|
58
|
+
return `tenants/${tenantId}/content/${relative}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function readPointer(
|
|
62
|
+
get: KvGet,
|
|
63
|
+
tenantId: string,
|
|
64
|
+
): Promise<PointerRecord | null> {
|
|
65
|
+
const raw = await get(customizationPointerKey(tenantId));
|
|
66
|
+
if (!raw) return null;
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(raw) as PointerRecord;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The `versionId` a channel currently points at, or null when the tenant has no
|
|
76
|
+
* channel pointer (i.e. is still on the legacy per-path model). Reads the pointer
|
|
77
|
+
* fresh; never throws.
|
|
78
|
+
*/
|
|
79
|
+
export async function resolveChannelVersion(
|
|
80
|
+
get: KvGet,
|
|
81
|
+
tenantId: string,
|
|
82
|
+
channel: ChannelName,
|
|
83
|
+
): Promise<string | null> {
|
|
84
|
+
const pointer = await readPointer(get, tenantId);
|
|
85
|
+
return pointer?.channels?.[channel] ?? null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Read one immutable site-version manifest, or null. Never throws. */
|
|
89
|
+
export async function readSiteVersion(
|
|
90
|
+
get: KvGet,
|
|
91
|
+
tenantId: string,
|
|
92
|
+
versionId: string,
|
|
93
|
+
): Promise<SiteVersionManifest | null> {
|
|
94
|
+
const raw = await get(siteVersionKey(tenantId, versionId));
|
|
95
|
+
if (!raw) return null;
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(raw) as SiteVersionManifest;
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolve a single artifact's content via the VERSIONED path:
|
|
105
|
+
* channel → versionId → site-version manifest → content hash → bytes. Returns
|
|
106
|
+
* null (so the caller falls back to build-time) when the tenant has no channel
|
|
107
|
+
* pointer, the version manifest is missing, the path isn't in the snapshot, or
|
|
108
|
+
* the content is gone. Never throws.
|
|
109
|
+
*/
|
|
110
|
+
export async function readVersionedArtifact(
|
|
111
|
+
get: KvGet,
|
|
112
|
+
tenantId: string,
|
|
113
|
+
channel: ChannelName,
|
|
114
|
+
path: string,
|
|
115
|
+
): Promise<string | null> {
|
|
116
|
+
const versionId = await resolveChannelVersion(get, tenantId, channel);
|
|
117
|
+
if (!versionId) return null;
|
|
118
|
+
const manifest = await readSiteVersion(get, tenantId, versionId);
|
|
119
|
+
const contentHash = manifest?.artifacts?.[path];
|
|
120
|
+
if (!contentHash) return null;
|
|
121
|
+
return get(customizationArtifactKey(tenantId, contentHash));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Read the published artifact content for (tenant, env, path), or null when
|
|
126
|
+
* nothing is published (no pointer, no selection for that path, or no stored
|
|
127
|
+
* content). Never throws on malformed data — returns null so the caller falls
|
|
128
|
+
* back to the build-time artifact.
|
|
129
|
+
*
|
|
130
|
+
* Resolution is versioned-first with a legacy fallback:
|
|
131
|
+
* - If the tenant has a channel pointer for the resolved channel
|
|
132
|
+
* (`channelFromEnv(env)`), resolve via the immutable site version. A channel
|
|
133
|
+
* pointer is AUTHORITATIVE — if the snapshot omits this path we return null
|
|
134
|
+
* (fall back to build-time), never the stale legacy selection.
|
|
135
|
+
* - Otherwise fall back to the LEGACY per-artifact-path selection
|
|
136
|
+
* (`pointer[env][path]`), so tenants not yet cut over are unaffected.
|
|
137
|
+
*/
|
|
138
|
+
export async function readPublishedArtifact(
|
|
139
|
+
get: KvGet,
|
|
140
|
+
tenantId: string,
|
|
141
|
+
env: CustomizationEnv,
|
|
142
|
+
path: string,
|
|
143
|
+
): Promise<string | null> {
|
|
144
|
+
const pointer = await readPointer(get, tenantId);
|
|
145
|
+
if (!pointer) return null;
|
|
146
|
+
|
|
147
|
+
const channel = channelFromEnv(env);
|
|
148
|
+
if (pointer.channels?.[channel]) {
|
|
149
|
+
// Versioned model engaged for this channel — authoritative.
|
|
150
|
+
return readVersionedArtifact(get, tenantId, channel, path);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Legacy per-path selection.
|
|
154
|
+
const versionId = pointer?.[env]?.[path];
|
|
155
|
+
if (!versionId) return null;
|
|
156
|
+
return get(customizationArtifactKey(tenantId, versionId));
|
|
157
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant browser-script vocabulary the renderer needs: the slot / strategy /
|
|
3
|
+
* isolation constants, the CSP hash helper, and the sandboxed widget iframe
|
|
4
|
+
* attributes the renderer draws (`components/WidgetFrame.astro`).
|
|
5
|
+
*
|
|
6
|
+
* Dependency-free + Workers-portable (Web Crypto, btoa).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const SCRIPT_SLOTS = [
|
|
10
|
+
"product.aside",
|
|
11
|
+
"home.section",
|
|
12
|
+
"global.footer",
|
|
13
|
+
] as const;
|
|
14
|
+
export const SCRIPT_STRATEGIES = ["defer", "on-idle", "on-interaction"] as const;
|
|
15
|
+
export const SCRIPT_ISOLATIONS = ["sandbox", "inline"] as const;
|
|
16
|
+
|
|
17
|
+
export type ScriptSlot = (typeof SCRIPT_SLOTS)[number];
|
|
18
|
+
export type ScriptStrategy = (typeof SCRIPT_STRATEGIES)[number];
|
|
19
|
+
export type ScriptIsolation = (typeof SCRIPT_ISOLATIONS)[number];
|
|
20
|
+
|
|
21
|
+
export interface ScriptRegistration {
|
|
22
|
+
src: string;
|
|
23
|
+
slot: ScriptSlot;
|
|
24
|
+
strategy: ScriptStrategy;
|
|
25
|
+
isolation: ScriptIsolation;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The base64 `'sha256-…'` CSP source expression that pins this exact bundle. */
|
|
29
|
+
export async function cspHash(content: string | Uint8Array): Promise<string> {
|
|
30
|
+
const bytes =
|
|
31
|
+
typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
32
|
+
const digest = await crypto.subtle.digest(
|
|
33
|
+
"SHA-256",
|
|
34
|
+
bytes as unknown as BufferSource,
|
|
35
|
+
);
|
|
36
|
+
let bin = "";
|
|
37
|
+
for (const b of new Uint8Array(digest)) bin += String.fromCharCode(b);
|
|
38
|
+
return `'sha256-${btoa(bin)}'`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Sandbox token list for tenant widgets: scripts only, **no
|
|
43
|
+
* `allow-same-origin`**. Omitting same-origin forces an opaque origin, so the
|
|
44
|
+
* widget is cross-origin to the page — it cannot read parent cookies/DOM, nor
|
|
45
|
+
* its own cookies/localStorage — even when served from the platform's own
|
|
46
|
+
* origin. That's why v1 needs no separate widget domain.
|
|
47
|
+
*/
|
|
48
|
+
export const WIDGET_SANDBOX = "allow-scripts";
|
|
49
|
+
|
|
50
|
+
export interface WidgetFrameAttrs {
|
|
51
|
+
sandbox: string;
|
|
52
|
+
loading: "eager" | "lazy";
|
|
53
|
+
referrerpolicy: "no-referrer";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Iframe attributes for a sandboxed widget, derived from its load strategy. */
|
|
57
|
+
export function widgetFrameAttrs(strategy: ScriptStrategy): WidgetFrameAttrs {
|
|
58
|
+
return {
|
|
59
|
+
sandbox: WIDGET_SANDBOX,
|
|
60
|
+
// "defer" loads with the page; idle/interaction defer to lazy (near-viewport).
|
|
61
|
+
loading: strategy === "defer" ? "eager" : "lazy",
|
|
62
|
+
referrerpolicy: "no-referrer",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pointer/selection **types** for published customizations, as the storefront
|
|
3
|
+
* read path sees them: which content version is selected for `live` vs `test`,
|
|
4
|
+
* per artifact path. Types only — nothing here reads, writes, or mutates state.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Metadata for one immutable, content-addressed artifact version. */
|
|
8
|
+
export interface ArtifactVersion {
|
|
9
|
+
tenantId: string;
|
|
10
|
+
/** Repo-relative tenant-owned path (e.g. themes/tenants/<id>.theme.json). */
|
|
11
|
+
path: string;
|
|
12
|
+
/** Content-addressed id: sha256(content) hex. */
|
|
13
|
+
versionId: string;
|
|
14
|
+
size: number;
|
|
15
|
+
createdAt: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Which version of each artifact is selected for an environment. */
|
|
19
|
+
export type Selection = Record<string, string>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A publish **channel**: the audience-facing surface a request resolves to.
|
|
23
|
+
* `live` is what production hosts serve; `preview` is what a gated preview host
|
|
24
|
+
* serves. (The middleware's `CustomizationEnv` "test" maps to the `preview`
|
|
25
|
+
* channel — see `channelFromEnv`.)
|
|
26
|
+
*/
|
|
27
|
+
export type ChannelName = "live" | "preview";
|
|
28
|
+
|
|
29
|
+
/** Channel → versionId. Optional/partial: a channel may be unset. */
|
|
30
|
+
export type ChannelPointers = Partial<Record<ChannelName, string>>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Per-tenant pointer record.
|
|
34
|
+
*
|
|
35
|
+
* - `live` / `test`: the LEGACY per-artifact-path selections (kept so tenants not
|
|
36
|
+
* yet cut over to the versioned model resolve exactly as before).
|
|
37
|
+
* - `channels`: the versioned-publish channel pointers (`live`/`preview` →
|
|
38
|
+
* a single site `versionId`). Additive + optional; when present it is
|
|
39
|
+
* authoritative for that channel and shadows the legacy selection.
|
|
40
|
+
* - `rev`: optimistic-concurrency token, incremented on every successful write.
|
|
41
|
+
*/
|
|
42
|
+
export interface PointerRecord {
|
|
43
|
+
tenantId: string;
|
|
44
|
+
rev: number;
|
|
45
|
+
live: Selection;
|
|
46
|
+
test: Selection;
|
|
47
|
+
channels?: ChannelPointers;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function emptyPointer(tenantId: string): PointerRecord {
|
|
51
|
+
return { tenantId, rev: 0, live: {}, test: {} };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Map the middleware's customization env to a publish channel. Production hosts
|
|
56
|
+
* resolve `live` → `live`; a gated preview resolves `test` → `preview`.
|
|
57
|
+
*/
|
|
58
|
+
export function channelFromEnv(env: "live" | "test"): ChannelName {
|
|
59
|
+
return env === "live" ? "live" : "preview";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A reference to a tenant's public-asset bundle for a version. The asset lane
|
|
64
|
+
* stores the BYTES in R2 keyed by this same `versionId`; the site-version
|
|
65
|
+
* manifest carries only the reference (publish never writes asset bytes).
|
|
66
|
+
*/
|
|
67
|
+
export interface AssetBundleRef {
|
|
68
|
+
versionId: string;
|
|
69
|
+
count?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* An immutable, content-addressed snapshot of a tenant's whole site, keyed by a
|
|
74
|
+
* single `versionId` (a Gitea commit sha, or a content hash when there is no
|
|
75
|
+
* commit). `artifacts` maps each artifact path to the sha256 of its content; the
|
|
76
|
+
* bytes live at `cust:artifact:<tenant>:<contentHash>` (the existing, deduped
|
|
77
|
+
* content store). `assets` references the public-asset bundle (bytes owned by the
|
|
78
|
+
* asset lane's R2). A version is written once and never mutated.
|
|
79
|
+
*/
|
|
80
|
+
export interface SiteVersionManifest {
|
|
81
|
+
tenantId: string;
|
|
82
|
+
versionId: string;
|
|
83
|
+
source: "commit" | "hash";
|
|
84
|
+
createdAt: string;
|
|
85
|
+
artifacts: Record<string, string>;
|
|
86
|
+
assets?: AssetBundleRef;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Compact list-index entry for version history (KV has no cheap scan). */
|
|
90
|
+
export interface SiteVersionSummary {
|
|
91
|
+
versionId: string;
|
|
92
|
+
source: "commit" | "hash";
|
|
93
|
+
createdAt: string;
|
|
94
|
+
artifactCount: number;
|
|
95
|
+
}
|