@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,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/dev/diff?path= — Phase 2 (cp-diff) of the hosted /dev cockpit
|
|
3
|
+
* (workstream tot-lime-cloud-dev-cockpit, decision
|
|
4
|
+
* architecture-environment-aware-cockpit). Diffs the developer's draft
|
|
5
|
+
* overlay against published for one path, returning the SAME shape as the
|
|
6
|
+
* local loop's `/__tot/diff` (scripts/dev/git-status.mjs handleDiff —
|
|
7
|
+
* {ok,path,status,shown,before,after}) so dev.astro's existing diff UI
|
|
8
|
+
* (doDiff/renderDiff) needs no client-side change.
|
|
9
|
+
*
|
|
10
|
+
* No git in the Worker — "status" derives from draft-present-vs-published
|
|
11
|
+
* instead of `git status`: "unchanged" (no draft, or draft equals
|
|
12
|
+
* published), "modified" (draft differs from an existing published value),
|
|
13
|
+
* "added" (a draft exists for a path with no published counterpart yet —
|
|
14
|
+
* e.g. a brand-new pages-html/<slug>.html the developer hasn't published).
|
|
15
|
+
* Both sides are the raw-HTML/JSON writable surface, always text — no binary
|
|
16
|
+
* degrade case exists here (unlike the local loop's arbitrary file tree).
|
|
17
|
+
*/
|
|
18
|
+
import type { APIContext } from "astro";
|
|
19
|
+
import { json } from "@/lib/grants/route.js";
|
|
20
|
+
import {
|
|
21
|
+
resolveCockpitContext,
|
|
22
|
+
isCockpitContext,
|
|
23
|
+
isWritablePath,
|
|
24
|
+
readPublishedPath,
|
|
25
|
+
resolveDraftState,
|
|
26
|
+
} from "@/lib/dev/hostedCockpit";
|
|
27
|
+
|
|
28
|
+
export const prerender = false;
|
|
29
|
+
|
|
30
|
+
const MAX_DIFF_BYTES = 512 * 1024;
|
|
31
|
+
|
|
32
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
33
|
+
const ctx = await resolveCockpitContext(context);
|
|
34
|
+
if (!isCockpitContext(ctx)) return ctx;
|
|
35
|
+
|
|
36
|
+
const path = new URL(context.request.url).searchParams.get("path");
|
|
37
|
+
if (!path) return json({ ok: false, status: "no_path", message: "path query param is required" }, 400);
|
|
38
|
+
if (!isWritablePath(path)) {
|
|
39
|
+
return json({ ok: false, status: "bad_path", message: `Not available on the hosted panel: ${path}` }, 400);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const [published, { draftContent }] = await Promise.all([
|
|
43
|
+
readPublishedPath(ctx, path),
|
|
44
|
+
resolveDraftState(context, ctx, path),
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
if (published === null && draftContent === null) {
|
|
48
|
+
return json({ ok: false, status: "not_found", message: `No such file: ${path}` }, 404);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const status =
|
|
52
|
+
draftContent === null
|
|
53
|
+
? "unchanged"
|
|
54
|
+
: published === null
|
|
55
|
+
? "added"
|
|
56
|
+
: draftContent === published
|
|
57
|
+
? "unchanged"
|
|
58
|
+
: "modified";
|
|
59
|
+
|
|
60
|
+
const before = published ?? "";
|
|
61
|
+
const after = draftContent ?? published ?? "";
|
|
62
|
+
const size = new TextEncoder().encode(before + after).length;
|
|
63
|
+
if (size > MAX_DIFF_BYTES) {
|
|
64
|
+
return json({ ok: true, path, status, shown: false, reason: "too_big" });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return json({ ok: true, path, status, shown: true, before, after });
|
|
68
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/dev/file?path= — Phase 1 (cp-file) of the hosted /dev cockpit
|
|
3
|
+
* (workstream tot-lime-cloud-dev-cockpit, decision
|
|
4
|
+
* architecture-environment-aware-cockpit). Reads one file off the same
|
|
5
|
+
* synthesized tree cp-tree (/api/dev/tree) lists, mirroring the local loop's
|
|
6
|
+
* `/__tot/file` response contract (scripts/dev/file-browser.mjs readFile/
|
|
7
|
+
* handleFile) so dev.astro's filesScript viewer needs no client-side change.
|
|
8
|
+
*
|
|
9
|
+
* `path` is one of the three shapes cp-tree ever lists — content/home.html,
|
|
10
|
+
* content/pages-html/<slug>.html, or theme.json (scripts/dev/safe-path.mjs
|
|
11
|
+
* WRITE_ALLOWLIST) — so this route is a closed switch on those shapes, not a
|
|
12
|
+
* general path resolver; anything else is a clean 400, never a traversal risk.
|
|
13
|
+
*
|
|
14
|
+
* Reads the developer's OWN draft overlay first (readEffectivePath), falling
|
|
15
|
+
* back to published — so re-opening a file after a cp-write save shows the
|
|
16
|
+
* edit, and the `mtimeMs` it hands back is the SAME contentVersion() cp-write
|
|
17
|
+
* checks on the next save (the C6 stale-write guard).
|
|
18
|
+
*
|
|
19
|
+
* `writable:true` on all three is intentional now that cp-write exists: this
|
|
20
|
+
* is genuinely the editable surface.
|
|
21
|
+
*/
|
|
22
|
+
import type { APIContext } from "astro";
|
|
23
|
+
import { json } from "@/lib/grants/route.js";
|
|
24
|
+
import {
|
|
25
|
+
resolveCockpitContext,
|
|
26
|
+
isCockpitContext,
|
|
27
|
+
isWritablePath,
|
|
28
|
+
languageForPath,
|
|
29
|
+
readEffectivePath,
|
|
30
|
+
contentVersion,
|
|
31
|
+
} from "@/lib/dev/hostedCockpit";
|
|
32
|
+
|
|
33
|
+
export const prerender = false;
|
|
34
|
+
|
|
35
|
+
const MAX_FILE_BYTES = 512 * 1024;
|
|
36
|
+
|
|
37
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
38
|
+
const ctx = await resolveCockpitContext(context);
|
|
39
|
+
if (!isCockpitContext(ctx)) return ctx;
|
|
40
|
+
|
|
41
|
+
const path = new URL(context.request.url).searchParams.get("path");
|
|
42
|
+
if (!path) return json({ ok: false, status: "no_path", message: "path query param is required" }, 400);
|
|
43
|
+
if (!isWritablePath(path)) {
|
|
44
|
+
return json({ ok: false, status: "bad_path", message: `Not available on the hosted panel: ${path}` }, 400);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { content } = await readEffectivePath(context, ctx, path);
|
|
48
|
+
if (content === null) return json({ ok: false, status: "not_found", message: `No such file: ${path}` }, 404);
|
|
49
|
+
|
|
50
|
+
const language = languageForPath(path);
|
|
51
|
+
const mtimeMs = contentVersion(content);
|
|
52
|
+
const size = new TextEncoder().encode(content).length;
|
|
53
|
+
if (size > MAX_FILE_BYTES) {
|
|
54
|
+
return json({ ok: true, status: "ok", path, tooBig: true, size, language, content: null, mtimeMs, writable: true });
|
|
55
|
+
}
|
|
56
|
+
return json({ ok: true, status: "ok", path, size, language, content, mtimeMs, writable: true });
|
|
57
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/dev/locate — Phase 2 (cp-locate) of the hosted /dev cockpit
|
|
3
|
+
* (workstream tot-lime-cloud-dev-cockpit, decision
|
|
4
|
+
* architecture-environment-aware-cockpit). Click-to-source in the Worker:
|
|
5
|
+
* given a clicked preview URL + captured DOM path, resolve the source
|
|
6
|
+
* line:col in the developer's raw-HTML content, using the exact SAME pure
|
|
7
|
+
* parse5 walk as the local loop (scripts/dev/locate.mjs `locate()`) and the
|
|
8
|
+
* same URL -> content-path mapping (scripts/dev/locate-handler.mjs
|
|
9
|
+
* `resolvePageSource`), both imported directly (dependency-free besides
|
|
10
|
+
* parse5, safe in the Worker).
|
|
11
|
+
*
|
|
12
|
+
* Honors decision click-to-source-anchors-at-content-region from the
|
|
13
|
+
* local-loop board: a click resolves against the raw content region only; the
|
|
14
|
+
* storefront's own chrome (header/nav/footer, not in tenant source) refuses
|
|
15
|
+
* explicitly with `not_in_content`, never a coincidental miss.
|
|
16
|
+
*
|
|
17
|
+
* Unlike the local route, this reads the developer's DRAFT overlay first
|
|
18
|
+
* (readEffectivePath) — correct, since that's what their preview iframe is
|
|
19
|
+
* actually showing once they've saved an edit (cp-write). No open-in-editor
|
|
20
|
+
* equivalent: the jump lands in the in-cockpit editor via the existing
|
|
21
|
+
* tot:locate postMessage handler (c8Script), already wired and unchanged.
|
|
22
|
+
*
|
|
23
|
+
* Only `locate()` itself is imported from scripts/dev/locate.mjs (pure,
|
|
24
|
+
* parse5-only — Workers-safe). The URL->content-path mapping
|
|
25
|
+
* (resolvePageSource in scripts/dev/locate-handler.mjs) is reimplemented
|
|
26
|
+
* here rather than imported, because that module also pulls in node:fs +
|
|
27
|
+
* node:child_process (open-in-editor) which have no place in a Worker
|
|
28
|
+
* bundle; isWritablePath (hostedCockpit.ts) already IS this route's write
|
|
29
|
+
* allowlist, so the two definitions can't drift.
|
|
30
|
+
*
|
|
31
|
+
* NOTE (scope): this ships the server contract only. The client picker
|
|
32
|
+
* widget (src/lib/dev/ai-widget-client.js) that lets a developer click an
|
|
33
|
+
* element in the preview has no hosted injection path yet (dev-only today,
|
|
34
|
+
* see rawMarketingHtml.ts) — tracked separately as cp-locate-widget-injection
|
|
35
|
+
* so that security-sensitive gating gets its own review rather than riding
|
|
36
|
+
* in here.
|
|
37
|
+
*/
|
|
38
|
+
import type { APIContext } from "astro";
|
|
39
|
+
import { json } from "@/lib/grants/route.js";
|
|
40
|
+
import { locate } from "../../../../../../scripts/dev/locate.mjs";
|
|
41
|
+
import {
|
|
42
|
+
resolveCockpitContext,
|
|
43
|
+
isCockpitContext,
|
|
44
|
+
readEffectivePath,
|
|
45
|
+
isWritablePath,
|
|
46
|
+
} from "@/lib/dev/hostedCockpit";
|
|
47
|
+
|
|
48
|
+
export const prerender = false;
|
|
49
|
+
|
|
50
|
+
interface LocateBody {
|
|
51
|
+
path?: unknown;
|
|
52
|
+
domPath?: unknown;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Map a clicked preview URL to its raw-HTML content source, root-relative.
|
|
57
|
+
* Mirrors scripts/dev/locate-handler.mjs's resolvePageSource exactly (see
|
|
58
|
+
* that module's header for the mapping rules); duplicated rather than
|
|
59
|
+
* imported for the node:fs-free-Worker-bundle reason above.
|
|
60
|
+
*/
|
|
61
|
+
function resolvePreviewPageSource(pathname: string, appDomain: string): string | null {
|
|
62
|
+
let clean: string;
|
|
63
|
+
try {
|
|
64
|
+
clean = decodeURIComponent(pathname.split("?")[0]!.split("#")[0]!);
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const segments = clean.split("/").filter(Boolean);
|
|
69
|
+
if (segments.length && segments[0] === appDomain) segments.shift();
|
|
70
|
+
if (segments.some((s) => s === "..")) return null;
|
|
71
|
+
const rel = segments.length === 0 ? "content/home.html" : `content/pages-html/${segments.join("/")}.html`;
|
|
72
|
+
return isWritablePath(rel) ? rel : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isDomPath(domPath: unknown): domPath is Array<{ tag: string; index: number }> {
|
|
76
|
+
return (
|
|
77
|
+
Array.isArray(domPath) &&
|
|
78
|
+
domPath.length > 0 &&
|
|
79
|
+
domPath.every(
|
|
80
|
+
(s) => s && typeof s === "object" && typeof (s as any).tag === "string" && Number.isFinite(Number((s as any).index)),
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
86
|
+
const ctx = await resolveCockpitContext(context);
|
|
87
|
+
if (!isCockpitContext(ctx)) return ctx;
|
|
88
|
+
|
|
89
|
+
let body: LocateBody;
|
|
90
|
+
try {
|
|
91
|
+
body = (await context.request.json()) as LocateBody;
|
|
92
|
+
} catch {
|
|
93
|
+
return json({ ok: false, status: "bad_json", message: "invalid JSON body" }, 400);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!isDomPath(body.domPath)) {
|
|
97
|
+
return json({ ok: false, status: "bad_dom_path", message: "domPath must be a non-empty array of {tag,index}" }, 400);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const rel = typeof body.path === "string" ? resolvePreviewPageSource(body.path, ctx.tenant.appDomain) : null;
|
|
101
|
+
if (!rel) {
|
|
102
|
+
return json({
|
|
103
|
+
ok: false,
|
|
104
|
+
status: "not_raw_html",
|
|
105
|
+
message: "This page isn’t a raw-HTML content page — click-to-source is for content/*.html tenants.",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { content } = await readEffectivePath(context, ctx, rel);
|
|
110
|
+
if (content === null) {
|
|
111
|
+
return json({ ok: false, status: "not_raw_html", message: "No raw-HTML source backs this page." });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const found = locate(content, body.domPath);
|
|
115
|
+
if (typeof (found as { line?: number }).line === "number") {
|
|
116
|
+
const { line, col } = found as { line: number; col: number };
|
|
117
|
+
return json({ ok: true, path: rel, line, col });
|
|
118
|
+
}
|
|
119
|
+
return json({
|
|
120
|
+
ok: false,
|
|
121
|
+
status: "not_in_content",
|
|
122
|
+
message:
|
|
123
|
+
"That element is part of the storefront frame (header, nav, or footer), not your content source. Click something inside your page content.",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/dev/publish — Phase 3 (cp-publish) of the hosted /dev cockpit
|
|
3
|
+
* (workstream tot-lime-cloud-dev-cockpit). Publishes the developer's current
|
|
4
|
+
* draft content for one file into the tenant's TEST channel — the moment the
|
|
5
|
+
* private draft overlay (cp-write) becomes something OTHER viewers can see
|
|
6
|
+
* (still not live).
|
|
7
|
+
*
|
|
8
|
+
* SCOPE (see decision cp-publish-scope-correction): the card's literal plan
|
|
9
|
+
* ("wire to api/preview/promote.ts") doesn't fit this surface — that route
|
|
10
|
+
* needs a server-to-server bearer secret + a CDP-driven compliance
|
|
11
|
+
* GateReport, and operates on the Gitea-fed preview/live channel, not the
|
|
12
|
+
* dev-draft: overlay. Instead this uses the lower-level primitive the
|
|
13
|
+
* reconcile pipeline itself is built on — `publishToTest`
|
|
14
|
+
* (private-controlplane/customization-versioning.ts) over `KVVersionStore` —
|
|
15
|
+
* writing straight into the SAME published-artifact store
|
|
16
|
+
* `readPublishedArtifact`/`PublishedContentProvider` read. Content-addressed
|
|
17
|
+
* + idempotent by construction (re-publishing identical bytes is a no-op).
|
|
18
|
+
*
|
|
19
|
+
* Deliberately test-only: promoting test -> live is the real external-publish
|
|
20
|
+
* action and isn't reachable from this surface (see decision + the dispatched
|
|
21
|
+
* cp-publish-go-live follow-up). "Publish" here closes write -> preview ->
|
|
22
|
+
* diff -> publish-to-test, honestly, without faking or bypassing the
|
|
23
|
+
* existing compliance gate.
|
|
24
|
+
*/
|
|
25
|
+
import type { APIContext } from "astro";
|
|
26
|
+
import { json } from "@/lib/grants/route.js";
|
|
27
|
+
import { readKv } from "@/lib/env";
|
|
28
|
+
import {
|
|
29
|
+
publishToTest,
|
|
30
|
+
CustomizationError,
|
|
31
|
+
KVVersionStore,
|
|
32
|
+
type CustomizationManifest,
|
|
33
|
+
} from "@tot/private-controlplane";
|
|
34
|
+
import { contentArtifactPath, themeArtifactPath } from "@tot/public-runtime";
|
|
35
|
+
import {
|
|
36
|
+
resolveCockpitContext,
|
|
37
|
+
isCockpitContext,
|
|
38
|
+
isWritablePath,
|
|
39
|
+
pageSlugForPath,
|
|
40
|
+
resolveDraftState,
|
|
41
|
+
} from "@/lib/dev/hostedCockpit";
|
|
42
|
+
|
|
43
|
+
export const prerender = false;
|
|
44
|
+
|
|
45
|
+
// The developer's editable raw-HTML/theme surface, in ARTIFACT-path shape
|
|
46
|
+
// (tenants/<id>/...) — what publishToTest's isEditable check and the
|
|
47
|
+
// published-artifact store both key on. Not the tenant's real
|
|
48
|
+
// declarative-customization manifest (out of scope here): this is
|
|
49
|
+
// defense-in-depth on top of the isWritablePath gate already enforced below.
|
|
50
|
+
const PUBLISH_MANIFEST: CustomizationManifest = {
|
|
51
|
+
version: 1,
|
|
52
|
+
classes: {
|
|
53
|
+
"platform-owned": [],
|
|
54
|
+
"extension-point": [],
|
|
55
|
+
"tenant-owned": ["tenants/<id>/content/home.html", "tenants/<id>/content/pages-html/**", "tenants/<id>/theme.json"],
|
|
56
|
+
},
|
|
57
|
+
immutableCompliance: [],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Map this route's short content path (content/home.html, theme.json, ...)
|
|
62
|
+
* to the artifact path the published-artifact store actually keys on
|
|
63
|
+
* (contentArtifactPath/themeArtifactPath — the SAME mapping
|
|
64
|
+
* PublishedContentProvider's getHomeHtml/getPageHtml use internally via
|
|
65
|
+
* `publishedRaw`). Only ever called after isWritablePath(path) passed, so
|
|
66
|
+
* every branch here has a defined result.
|
|
67
|
+
*/
|
|
68
|
+
function artifactPathFor(tenantId: string, path: string): string {
|
|
69
|
+
if (path === "theme.json") return themeArtifactPath(tenantId);
|
|
70
|
+
if (path === "content/home.html") return contentArtifactPath(tenantId, "home.html");
|
|
71
|
+
return contentArtifactPath(tenantId, `pages-html/${pageSlugForPath(path)}.html`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface PublishBody {
|
|
75
|
+
path?: unknown;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
79
|
+
const ctx = await resolveCockpitContext(context);
|
|
80
|
+
if (!isCockpitContext(ctx)) return ctx;
|
|
81
|
+
|
|
82
|
+
let body: PublishBody;
|
|
83
|
+
try {
|
|
84
|
+
body = (await context.request.json()) as PublishBody;
|
|
85
|
+
} catch {
|
|
86
|
+
return json({ ok: false, status: "bad_content", message: "invalid JSON body" }, 400);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const path = typeof body.path === "string" ? body.path : "";
|
|
90
|
+
if (!path) return json({ ok: false, status: "no_path", message: "path is required" }, 400);
|
|
91
|
+
if (!isWritablePath(path)) {
|
|
92
|
+
return json({ ok: false, status: "not_writable", message: `not in the write allowlist: ${path}` }, 403);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const { draftContent } = await resolveDraftState(context, ctx, path);
|
|
96
|
+
if (draftContent === null) {
|
|
97
|
+
return json(
|
|
98
|
+
{ ok: false, status: "no_draft", message: "You have no unpublished edit for this file — nothing to publish." },
|
|
99
|
+
409,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const kv = await readKv("TENANT_CACHE");
|
|
104
|
+
if (!kv) {
|
|
105
|
+
// No KV bound (e.g. bare local build) — accept but report unpersisted,
|
|
106
|
+
// matching cp-write's degraded-mode contract rather than a broken call.
|
|
107
|
+
return json({ ok: true, status: "ok", path, applied: false });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const store = new KVVersionStore(kv);
|
|
111
|
+
const artifactPath = artifactPathFor(ctx.tenant.tenant_id, path);
|
|
112
|
+
try {
|
|
113
|
+
const result = await publishToTest(store, PUBLISH_MANIFEST, ctx.tenant.tenant_id, artifactPath, draftContent);
|
|
114
|
+
return json({
|
|
115
|
+
ok: true,
|
|
116
|
+
status: "ok",
|
|
117
|
+
path,
|
|
118
|
+
applied: true,
|
|
119
|
+
versionId: result.artifact.versionId,
|
|
120
|
+
channel: "test",
|
|
121
|
+
});
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (err instanceof CustomizationError) {
|
|
124
|
+
return json({ ok: false, status: err.code, message: err.message }, 409);
|
|
125
|
+
}
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/dev/tree — Phase 1 (cp-tree) of the hosted /dev cockpit
|
|
3
|
+
* (workstream tot-lime-cloud-dev-cockpit, decision
|
|
4
|
+
* architecture-environment-aware-cockpit). Lists the signed-in developer's OWN
|
|
5
|
+
* tenant content so the hosted cockpit's Files tab renders the same tree shape
|
|
6
|
+
* the local loop's `/__tot/tree` does (see scripts/dev/file-browser.mjs
|
|
7
|
+
* buildTree/handleTree — this route mirrors its {ok,tree} response contract,
|
|
8
|
+
* consumed unchanged by dev.astro's filesScript via the cp-env apiBase resolver).
|
|
9
|
+
*
|
|
10
|
+
* Unlike the local tree (an arbitrary on-disk directory walk), the hosted
|
|
11
|
+
* Worker has no filesystem — the tree here is SYNTHESIZED from exactly the
|
|
12
|
+
* raw-HTML surface the content provider chain understands + the write
|
|
13
|
+
* allowlist (scripts/dev/safe-path.mjs WRITE_ALLOWLIST): content/home.html,
|
|
14
|
+
* content/pages-html/<slug>.html, and theme.json. A tenant on structured
|
|
15
|
+
* (Storyblok/JSON) content simply has an emptier tree — nothing to browse/edit
|
|
16
|
+
* on this raw-HTML surface yet.
|
|
17
|
+
*/
|
|
18
|
+
import type { APIContext } from "astro";
|
|
19
|
+
import { json } from "@/lib/grants/route.js";
|
|
20
|
+
import { readPublishedArtifact, themeArtifactPath } from "@tot/public-runtime";
|
|
21
|
+
import { resolveCockpitContext, isCockpitContext } from "@/lib/dev/hostedCockpit";
|
|
22
|
+
|
|
23
|
+
export const prerender = false;
|
|
24
|
+
|
|
25
|
+
type TreeNode =
|
|
26
|
+
| { name: string; path: string; type: "file"; language: string | null }
|
|
27
|
+
| { name: string; path: string; type: "dir"; children: TreeNode[] };
|
|
28
|
+
|
|
29
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
30
|
+
const ctx = await resolveCockpitContext(context);
|
|
31
|
+
if (!isCockpitContext(ctx)) return ctx;
|
|
32
|
+
const { tenant, provider, kvGet } = ctx;
|
|
33
|
+
|
|
34
|
+
const [homeHtml, pageSlugs, themeRaw] = await Promise.all([
|
|
35
|
+
provider.getHomeHtml(),
|
|
36
|
+
provider.listPageHtmlSlugs(),
|
|
37
|
+
readPublishedArtifact(kvGet, tenant.tenant_id, "live", themeArtifactPath(tenant.tenant_id)),
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const contentChildren: TreeNode[] = [];
|
|
41
|
+
if (homeHtml !== null) {
|
|
42
|
+
contentChildren.push({ name: "home.html", path: "content/home.html", type: "file", language: "html" });
|
|
43
|
+
}
|
|
44
|
+
if (pageSlugs.length > 0) {
|
|
45
|
+
contentChildren.push({
|
|
46
|
+
name: "pages-html",
|
|
47
|
+
path: "content/pages-html",
|
|
48
|
+
type: "dir",
|
|
49
|
+
children: pageSlugs
|
|
50
|
+
.slice()
|
|
51
|
+
.sort((a, b) => a.localeCompare(b))
|
|
52
|
+
.map((slug) => ({
|
|
53
|
+
name: `${slug}.html`,
|
|
54
|
+
path: `content/pages-html/${slug}.html`,
|
|
55
|
+
type: "file" as const,
|
|
56
|
+
language: "html",
|
|
57
|
+
})),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const tree: TreeNode[] = [];
|
|
62
|
+
if (contentChildren.length > 0) {
|
|
63
|
+
tree.push({ name: "content", path: "content", type: "dir", children: contentChildren });
|
|
64
|
+
}
|
|
65
|
+
if (themeRaw !== null) {
|
|
66
|
+
tree.push({ name: "theme.json", path: "theme.json", type: "file", language: "json" });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return json({ ok: true, tenantId: tenant.tenant_id, tree });
|
|
70
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/dev/write — Phase 2 (cp-write) of the hosted /dev cockpit
|
|
3
|
+
* (workstream tot-lime-cloud-dev-cockpit, decision
|
|
4
|
+
* architecture-environment-aware-cockpit). The heart of cloud editing: a
|
|
5
|
+
* direct text edit (not an AI prompt) written into the developer's PRIVATE
|
|
6
|
+
* draft overlay, reusing /api/content-edit/ai-edit.ts's exact materialization
|
|
7
|
+
* mechanism (dev-draft: KV keys + signed tot_dev_draft cookie the middleware
|
|
8
|
+
* overlay reads) — so the /dev live preview reflects the edit and no other
|
|
9
|
+
* viewer/channel is touched. Same request/response contract as the local
|
|
10
|
+
* loop's `/__tot/write` (scripts/dev/file-writer.mjs handleWrite), including
|
|
11
|
+
* the C6 baseVersion 409 conflict guard — but "version" here is
|
|
12
|
+
* contentVersion() (a content hash), not a filesystem mtime, since the Worker
|
|
13
|
+
* has none; cp-file hands back the same hash so the two agree.
|
|
14
|
+
*
|
|
15
|
+
* Deviates from file-writer.mjs in one place: a theme.json write must parse as
|
|
16
|
+
* JSON before it lands (mirrors ai-edit.ts's guard) — this draft overlay feeds
|
|
17
|
+
* the SAME live preview theme.json ai-edit writes, and a malformed draft would
|
|
18
|
+
* degrade that preview for the developer, unlike a plain on-disk write.
|
|
19
|
+
*
|
|
20
|
+
* When KV or the signing secret is unavailable (e.g. no CUSTOMIZATION_PREVIEW_SECRET
|
|
21
|
+
* configured for this env), the edit is accepted but not persisted —
|
|
22
|
+
* `applied:false`, exactly ai-edit's degraded-mode contract — never a broken call.
|
|
23
|
+
*/
|
|
24
|
+
import type { APIContext } from "astro";
|
|
25
|
+
import { json } from "@/lib/grants/route.js";
|
|
26
|
+
import { DEV_DRAFT_TTL_SECONDS, devDraftKey } from "@/lib/dev/devDraft";
|
|
27
|
+
import {
|
|
28
|
+
resolveCockpitContext,
|
|
29
|
+
isCockpitContext,
|
|
30
|
+
isWritablePath,
|
|
31
|
+
readPublishedPath,
|
|
32
|
+
resolveDraftState,
|
|
33
|
+
contentVersion,
|
|
34
|
+
setDraftCookie,
|
|
35
|
+
} from "@/lib/dev/hostedCockpit";
|
|
36
|
+
|
|
37
|
+
export const prerender = false;
|
|
38
|
+
|
|
39
|
+
const MAX_WRITE_BYTES = 2 * 1024 * 1024;
|
|
40
|
+
|
|
41
|
+
interface WriteBody {
|
|
42
|
+
path?: unknown;
|
|
43
|
+
content?: unknown;
|
|
44
|
+
baseVersion?: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
48
|
+
const ctx = await resolveCockpitContext(context);
|
|
49
|
+
if (!isCockpitContext(ctx)) return ctx;
|
|
50
|
+
|
|
51
|
+
let body: WriteBody;
|
|
52
|
+
try {
|
|
53
|
+
body = (await context.request.json()) as WriteBody;
|
|
54
|
+
} catch {
|
|
55
|
+
return json({ ok: false, status: "bad_content", message: "invalid JSON body" }, 400);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const path = typeof body.path === "string" ? body.path : "";
|
|
59
|
+
if (!path) return json({ ok: false, status: "no_path", message: "path is required" }, 400);
|
|
60
|
+
if (!isWritablePath(path)) {
|
|
61
|
+
return json({ ok: false, status: "not_writable", message: `not in the write allowlist: ${path}` }, 403);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const content = body.content;
|
|
65
|
+
if (typeof content !== "string") {
|
|
66
|
+
return json({ ok: false, status: "bad_content", message: "content must be a string" }, 400);
|
|
67
|
+
}
|
|
68
|
+
if (new TextEncoder().encode(content).length > MAX_WRITE_BYTES) {
|
|
69
|
+
return json({ ok: false, status: "too_large", message: "content exceeds the write size limit" }, 413);
|
|
70
|
+
}
|
|
71
|
+
if (path === "theme.json") {
|
|
72
|
+
try {
|
|
73
|
+
JSON.parse(content);
|
|
74
|
+
} catch {
|
|
75
|
+
return json({ ok: false, status: "bad_content", message: "theme.json must be valid JSON" }, 400);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Stale-write guard (C6): compare the caller's baseVersion against the
|
|
80
|
+
// developer's CURRENT value for this path (their draft if one exists, else
|
|
81
|
+
// published) — omit baseVersion to force-save, exactly the local contract.
|
|
82
|
+
const { draftId, draftContent } = await resolveDraftState(context, ctx, path);
|
|
83
|
+
const currentContent = draftContent !== null ? draftContent : await readPublishedPath(ctx, path);
|
|
84
|
+
const currentVersion = contentVersion(currentContent);
|
|
85
|
+
const baseVersion = typeof body.baseVersion === "number" ? body.baseVersion : undefined;
|
|
86
|
+
if (baseVersion !== undefined && currentVersion !== baseVersion) {
|
|
87
|
+
return json(
|
|
88
|
+
{
|
|
89
|
+
ok: false,
|
|
90
|
+
status: "conflict",
|
|
91
|
+
current: currentVersion,
|
|
92
|
+
message:
|
|
93
|
+
currentVersion === null
|
|
94
|
+
? `${path} was deleted since you opened it`
|
|
95
|
+
: `${path} changed since you opened it`,
|
|
96
|
+
},
|
|
97
|
+
409,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const bytes = new TextEncoder().encode(content).length;
|
|
102
|
+
const mtimeMs = contentVersion(content);
|
|
103
|
+
|
|
104
|
+
if (!ctx.kvPut || !ctx.draftSecret) {
|
|
105
|
+
// Nothing to persist against (e.g. bare local build with no KV/secret) —
|
|
106
|
+
// report success without pretending the draft landed, like ai-edit.
|
|
107
|
+
return json({ ok: true, status: "ok", path, bytes, mtimeMs, applied: false });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await ctx.kvPut(devDraftKey(ctx.tenant.tenant_id, draftId, path), content, {
|
|
111
|
+
expirationTtl: DEV_DRAFT_TTL_SECONDS,
|
|
112
|
+
});
|
|
113
|
+
await setDraftCookie(context, ctx, draftId);
|
|
114
|
+
|
|
115
|
+
return json({ ok: true, status: "ok", path, bytes, mtimeMs, applied: true });
|
|
116
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newsletter signup endpoint.
|
|
3
|
+
*
|
|
4
|
+
* ASSUMPTION (Phase 1): there is no ESP integration yet. We persist the email to
|
|
5
|
+
* the TENANT_CACHE KV namespace when the Cloudflare runtime binding is present
|
|
6
|
+
* (key `newsletter:<email>`), and otherwise just log. Wiring a real ESP
|
|
7
|
+
* (Klaviyo/Mailchimp) is a documented Phase 2 swap at the marked spot below.
|
|
8
|
+
*
|
|
9
|
+
* Robustness: a signup must NEVER 500. All failures degrade to a friendly
|
|
10
|
+
* thank-you so the visitor is never shown an error for handing us their email.
|
|
11
|
+
*/
|
|
12
|
+
import type { APIContext } from "astro";
|
|
13
|
+
import { readKv } from "@/lib/env";
|
|
14
|
+
|
|
15
|
+
// Minimal, permissive email shape check — we are not the source of truth for
|
|
16
|
+
// deliverability; the ESP validates properly downstream.
|
|
17
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
18
|
+
|
|
19
|
+
function thankYou(message: string, status = 200): Response {
|
|
20
|
+
const html = `<!doctype html>
|
|
21
|
+
<html lang="en">
|
|
22
|
+
<head>
|
|
23
|
+
<meta charset="utf-8" />
|
|
24
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
25
|
+
<meta name="robots" content="noindex" />
|
|
26
|
+
<title>Thank you</title>
|
|
27
|
+
<style>
|
|
28
|
+
:root { color-scheme: light dark; }
|
|
29
|
+
body { font-family: ui-serif, Georgia, serif; margin: 0; min-height: 100vh;
|
|
30
|
+
display: grid; place-items: center; padding: 2rem; text-align: center;
|
|
31
|
+
background: #f4f1ea; color: #2f3a34; }
|
|
32
|
+
.card { max-width: 30rem; }
|
|
33
|
+
h1 { font-size: clamp(1.6rem, 4vw, 2.2rem); margin: 0 0 0.75rem; }
|
|
34
|
+
p { color: #5a655e; line-height: 1.6; }
|
|
35
|
+
a { color: #2f3a34; font-weight: 600; }
|
|
36
|
+
</style>
|
|
37
|
+
</head>
|
|
38
|
+
<body>
|
|
39
|
+
<div class="card">
|
|
40
|
+
<h1>You're on the list.</h1>
|
|
41
|
+
<p>${message}</p>
|
|
42
|
+
<p><a href="/">Back to the shop →</a></p>
|
|
43
|
+
</div>
|
|
44
|
+
</body>
|
|
45
|
+
</html>`;
|
|
46
|
+
return new Response(html, {
|
|
47
|
+
status,
|
|
48
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
53
|
+
// Off for marketing tenants — they capture leads via external CTAs.
|
|
54
|
+
if (!context.locals.features.newsletter) {
|
|
55
|
+
return new Response(null, { status: 404 });
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const form = await context.request.formData();
|
|
59
|
+
const email = String(form.get("email") ?? "").trim().toLowerCase();
|
|
60
|
+
const source = String(form.get("source") ?? "unknown").trim();
|
|
61
|
+
|
|
62
|
+
if (!EMAIL_RE.test(email)) {
|
|
63
|
+
return thankYou(
|
|
64
|
+
"That address didn't look quite right — head back and try again.",
|
|
65
|
+
200,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Persist. Phase 1: KV stub. Phase 2: replace with an ESP API call here.
|
|
70
|
+
const kv = await readKv("TENANT_CACHE");
|
|
71
|
+
if (kv) {
|
|
72
|
+
await kv.put(
|
|
73
|
+
`newsletter:${email}`,
|
|
74
|
+
JSON.stringify({ email, source, at: new Date().toISOString() }),
|
|
75
|
+
);
|
|
76
|
+
} else {
|
|
77
|
+
// No KV binding (local/dev): log and move on.
|
|
78
|
+
console.log(`[newsletter] signup ${email} (source: ${source})`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Redirect back where they came from (or home) with a success flag, so the
|
|
82
|
+
// page can show its own confirmation in-context. Falls back to the
|
|
83
|
+
// thank-you page if there's no usable referer.
|
|
84
|
+
const referer = context.request.headers.get("referer");
|
|
85
|
+
if (referer) {
|
|
86
|
+
try {
|
|
87
|
+
const back = new URL(referer);
|
|
88
|
+
back.searchParams.set("subscribed", "1");
|
|
89
|
+
return Response.redirect(back.toString(), 303);
|
|
90
|
+
} catch {
|
|
91
|
+
// fall through to thank-you
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return thankYou("We'll send word when something worth knowing lands.");
|
|
95
|
+
} catch {
|
|
96
|
+
// Never 500 on a signup.
|
|
97
|
+
return thankYou("We'll send word when something worth knowing lands.");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
102
|
+
// Nothing to render here directly — bounce back to the shop.
|
|
103
|
+
return context.redirect("/", 303);
|
|
104
|
+
}
|