@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,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demo password gate (HTTP Basic Auth) for named hosts.
|
|
3
|
+
*
|
|
4
|
+
* A lightweight "put a password in front of it before we show it" gate for demo
|
|
5
|
+
* subdomains (e.g. shop.tokenoftrust.store). Deliberately DISTINCT from the
|
|
6
|
+
* ShipLoop customization-preview gate (resolveCustomizationEnv / `preview.` hosts
|
|
7
|
+
* + signed, tenant-bound tokens): this is a single shared static password,
|
|
8
|
+
* browser-native, covering every path + subresource on the host. It runs first
|
|
9
|
+
* in the middleware so a blocked host never reaches tenant resolution, the edge
|
|
10
|
+
* cache, or any render.
|
|
11
|
+
*
|
|
12
|
+
* Config is env-driven so gating/ungating a host or rotating the password needs
|
|
13
|
+
* NO code change:
|
|
14
|
+
* DEMO_GATE_HOSTS comma-separated hostnames to gate ("" / unset = none)
|
|
15
|
+
* DEMO_GATE_PASSWORD the shared password (set via `wrangler secret`; unset = off)
|
|
16
|
+
* DEMO_GATE_REALM optional Basic-Auth realm label (default "Demo")
|
|
17
|
+
*
|
|
18
|
+
* The username is IGNORED — share it as "any username, password: …", the usual
|
|
19
|
+
* demo pattern. If no password is configured the gate is a no-op everywhere.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const REALM_FALLBACK = "Demo";
|
|
23
|
+
|
|
24
|
+
/** Lowercase, trim, and drop any `:port` suffix so host matching is exact. */
|
|
25
|
+
export function normalizeHost(host: string): string {
|
|
26
|
+
return host.trim().toLowerCase().replace(/:\d+$/, "");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* True when `host` matches the comma-separated gate list (case/port-insensitive).
|
|
31
|
+
*
|
|
32
|
+
* Entries are either an exact host (`storefront.tokenoftrust.workers.dev`) or a
|
|
33
|
+
* leading-`*.` wildcard (`*.tokenoftrust.store`) that matches ANY subdomain of
|
|
34
|
+
* that zone at any depth — `shop.tokenoftrust.store`,
|
|
35
|
+
* `storefront.tokenoftrust.store`, `preview.acme.tokenoftrust.store` — but NOT
|
|
36
|
+
* the bare apex. Wildcards are what let us gate every tenant (present + future)
|
|
37
|
+
* on a shared zone without enumerating each host.
|
|
38
|
+
*/
|
|
39
|
+
export function hostInGateList(host: string, hostsCsv: string | null | undefined): boolean {
|
|
40
|
+
const h = normalizeHost(host);
|
|
41
|
+
const list = (hostsCsv ?? "").split(",").map(normalizeHost).filter(Boolean);
|
|
42
|
+
for (const entry of list) {
|
|
43
|
+
if (entry.startsWith("*.")) {
|
|
44
|
+
const suffix = entry.slice(1); // ".tokenoftrust.store"
|
|
45
|
+
if (h.length > suffix.length && h.endsWith(suffix)) return true;
|
|
46
|
+
} else if (h === entry) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Parse the password out of an `Authorization: Basic …` header; null if absent/malformed. */
|
|
54
|
+
function passwordFromAuthHeader(header: string | null): string | null {
|
|
55
|
+
if (!header) return null;
|
|
56
|
+
const [scheme, encoded] = header.split(" ");
|
|
57
|
+
if (!scheme || scheme.toLowerCase() !== "basic" || !encoded) return null;
|
|
58
|
+
let decoded: string;
|
|
59
|
+
try {
|
|
60
|
+
decoded = atob(encoded);
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const colon = decoded.indexOf(":");
|
|
65
|
+
if (colon === -1) return null;
|
|
66
|
+
return decoded.slice(colon + 1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Length-hiding constant-time-ish string equality (avoids early-exit timing leaks). */
|
|
70
|
+
function safeEqual(a: string, b: string): boolean {
|
|
71
|
+
const len = Math.max(a.length, b.length);
|
|
72
|
+
let diff = a.length ^ b.length;
|
|
73
|
+
for (let i = 0; i < len; i++) {
|
|
74
|
+
diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
|
|
75
|
+
}
|
|
76
|
+
return diff === 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface DemoGateEnv {
|
|
80
|
+
hosts?: string | null;
|
|
81
|
+
password?: string | null;
|
|
82
|
+
realm?: string | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Decide whether a request to `host` is allowed past the demo gate.
|
|
87
|
+
* Returns a 401 Response to send when the gate blocks; null when the host is
|
|
88
|
+
* ungated, the gate is disabled, or valid credentials were supplied.
|
|
89
|
+
*/
|
|
90
|
+
export function evaluateDemoGate(
|
|
91
|
+
host: string,
|
|
92
|
+
authorization: string | null,
|
|
93
|
+
env: DemoGateEnv,
|
|
94
|
+
): Response | null {
|
|
95
|
+
const password = env.password?.trim();
|
|
96
|
+
if (!password) return null; // no password configured → gate disabled globally
|
|
97
|
+
|
|
98
|
+
if (!hostInGateList(host, env.hosts)) return null; // this host isn't gated
|
|
99
|
+
|
|
100
|
+
const supplied = passwordFromAuthHeader(authorization);
|
|
101
|
+
if (supplied !== null && safeEqual(supplied, password)) return null; // authorized
|
|
102
|
+
|
|
103
|
+
const realm = (env.realm?.trim() || REALM_FALLBACK).replace(/"/g, "");
|
|
104
|
+
return new Response("Authentication required.", {
|
|
105
|
+
status: 401,
|
|
106
|
+
headers: {
|
|
107
|
+
"WWW-Authenticate": `Basic realm="${realm}", charset="UTF-8"`,
|
|
108
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
109
|
+
// Never let a browser/CDN cache the challenge or a would-be authorized body.
|
|
110
|
+
"Cache-Control": "no-store",
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opaque, SESSION-KV-backed credential that lets a developer's LOCAL dev-loop
|
|
3
|
+
* process authenticate POST /api/dev/activity on the hosted Worker — the
|
|
4
|
+
* local→hosted activity bridge (handoff
|
|
5
|
+
* 2026-07-14-hosted-dev-activity-bridge-and-stepper).
|
|
6
|
+
*
|
|
7
|
+
* Minted alongside the existing cli-signin-code paste
|
|
8
|
+
* (apps/storefront/src/lib/dev/cliSignInCode.ts) and cached by the CLI in
|
|
9
|
+
* ~/.tot/credentials.json. Deliberately NOT a signed token: this reuses the
|
|
10
|
+
* SESSION KV binding that already exists in every environment (dev/qa/prod),
|
|
11
|
+
* so it needs no new secret to provision. Revoking is just deleting the KV
|
|
12
|
+
* entry — the same posture as the browser's own tot_session cookie (see
|
|
13
|
+
* lib/auth/session.ts).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const KEY_PREFIX = "dev-activity-token:";
|
|
17
|
+
/** Secondary index: developer+tenant -> their current live token, so a re-mint can
|
|
18
|
+
* revoke the previous one instead of accumulating an unbounded number of live
|
|
19
|
+
* 30-day credentials (every fresh invite/sign-in paste mints a new one). */
|
|
20
|
+
const INDEX_PREFIX = "dev-activity-token-by-dev:";
|
|
21
|
+
/** 30 days — re-minted on every fresh invite/sign-in paste, so this rarely lapses in practice. */
|
|
22
|
+
const TTL_SECONDS = 30 * 24 * 3600;
|
|
23
|
+
|
|
24
|
+
export interface ActivityIngestClaims {
|
|
25
|
+
email: string;
|
|
26
|
+
tenant: string;
|
|
27
|
+
capability: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function newToken(): string {
|
|
31
|
+
const bytes = new Uint8Array(24);
|
|
32
|
+
crypto.getRandomValues(bytes);
|
|
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 indexKey(email: string, tenant: string): string {
|
|
39
|
+
return `${INDEX_PREFIX}${tenant}:${email}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Mint a fresh activity-ingest token scoped to this developer + their own tenant.
|
|
44
|
+
* Revokes any token previously minted for the SAME developer+tenant first, so
|
|
45
|
+
* repeated invite/sign-in pastes leave at most one live credential per
|
|
46
|
+
* developer+tenant instead of accumulating indefinitely.
|
|
47
|
+
*/
|
|
48
|
+
export async function mintActivityIngestToken(
|
|
49
|
+
kv: KVNamespace,
|
|
50
|
+
claims: ActivityIngestClaims,
|
|
51
|
+
): Promise<{ token: string; expiresAt: string }> {
|
|
52
|
+
const idxKey = indexKey(claims.email, claims.tenant);
|
|
53
|
+
const priorToken = await kv.get(idxKey);
|
|
54
|
+
if (priorToken) await kv.delete(KEY_PREFIX + priorToken);
|
|
55
|
+
|
|
56
|
+
const token = newToken();
|
|
57
|
+
const expiresAt = new Date(Date.now() + TTL_SECONDS * 1000).toISOString();
|
|
58
|
+
await kv.put(KEY_PREFIX + token, JSON.stringify(claims), { expirationTtl: TTL_SECONDS });
|
|
59
|
+
await kv.put(idxKey, token, { expirationTtl: TTL_SECONDS });
|
|
60
|
+
return { token, expiresAt };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Verify a bearer token from a local dev-loop POST. Null when absent/expired/malformed. */
|
|
64
|
+
export async function verifyActivityIngestToken(
|
|
65
|
+
kv: KVNamespace,
|
|
66
|
+
token: string,
|
|
67
|
+
): Promise<ActivityIngestClaims | null> {
|
|
68
|
+
if (!token) return null;
|
|
69
|
+
const raw = await kv.get(KEY_PREFIX + token);
|
|
70
|
+
if (!raw) return null;
|
|
71
|
+
try {
|
|
72
|
+
const claims = JSON.parse(raw) as ActivityIngestClaims;
|
|
73
|
+
if (!claims?.tenant || !claims?.email) return null;
|
|
74
|
+
return claims;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-vendor "recent activity" feed for the hosted /dev panel — a small ring
|
|
3
|
+
* buffer of file-save events reported by a developer's LOCAL dev loop (see
|
|
4
|
+
* activityIngestToken.ts for the write-path auth). File-level only, per
|
|
5
|
+
* decision (no per-line diff, no separate published-history tier — see
|
|
6
|
+
* handoff 2026-07-14-hosted-dev-activity-bridge-and-stepper).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type ActivityEventKind = "change" | "add" | "unlink" | "up";
|
|
10
|
+
|
|
11
|
+
export interface ActivityEvent {
|
|
12
|
+
/** Tenant-relative file path for content events; omitted for status pings ("up"). */
|
|
13
|
+
file?: string;
|
|
14
|
+
event: ActivityEventKind;
|
|
15
|
+
/** Epoch ms, as reported by the local loop. */
|
|
16
|
+
at: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const KEY_PREFIX = "dev-activity:";
|
|
20
|
+
const MAX_EVENTS = 30;
|
|
21
|
+
/** Ephemeral, session-scoped buffer — v1 scope (file-level only, no durable history). */
|
|
22
|
+
const TTL_SECONDS = 24 * 3600;
|
|
23
|
+
|
|
24
|
+
function key(appDomain: string): string {
|
|
25
|
+
return KEY_PREFIX + appDomain;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Read the vendor's recent activity, oldest first. Never throws. */
|
|
29
|
+
export async function readActivity(kv: KVNamespace, appDomain: string): Promise<ActivityEvent[]> {
|
|
30
|
+
try {
|
|
31
|
+
const raw = await kv.get(key(appDomain));
|
|
32
|
+
if (!raw) return [];
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Append one reported event to the vendor's ring buffer (bounded, TTL'd). */
|
|
41
|
+
export async function appendActivity(
|
|
42
|
+
kv: KVNamespace,
|
|
43
|
+
appDomain: string,
|
|
44
|
+
ev: ActivityEvent,
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
const existing = await readActivity(kv, appDomain);
|
|
47
|
+
const next = [...existing, ev].slice(-MAX_EVENTS);
|
|
48
|
+
await kv.put(key(appDomain), JSON.stringify(next), { expirationTtl: TTL_SECONDS });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Business-readable translation of a reported file path + event, for the
|
|
53
|
+
* activity log. File-level only — no per-line diff enrichment (Darrin,
|
|
54
|
+
* 2026-07-14: "we can show that in step 2").
|
|
55
|
+
*/
|
|
56
|
+
export function describeActivity(file: string | undefined, event: ActivityEventKind): string {
|
|
57
|
+
if (event === "up") return "Your store is running locally";
|
|
58
|
+
const f = (file || "").replace(/^\/+/, "");
|
|
59
|
+
const verb = event === "add" ? "added" : event === "unlink" ? "removed" : "edited";
|
|
60
|
+
|
|
61
|
+
if (f === "content/home.html") return `Home page ${verb}`;
|
|
62
|
+
if (f.startsWith("content/") && f.endsWith(".html")) {
|
|
63
|
+
const page = f.slice("content/".length, -".html".length);
|
|
64
|
+
return `Page "${page}" ${verb}`;
|
|
65
|
+
}
|
|
66
|
+
if (f.startsWith("content/") && f.endsWith(".json")) {
|
|
67
|
+
return event === "change" ? "Page content updated" : `Page content ${verb}`;
|
|
68
|
+
}
|
|
69
|
+
if (f === "theme.json" || /theme.*\.css$/i.test(f)) return "Branding/theme changed";
|
|
70
|
+
if (f.startsWith("public/")) return `Image/asset ${verb}`;
|
|
71
|
+
return `${f} ${verb}`;
|
|
72
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// The floating "✦ ask" affordance for `tot dev`'s inline-AI edit loop (WS5 G1).
|
|
2
|
+
// Injected into every page ONLY in `astro dev` by inlineAiEditPlugin in
|
|
3
|
+
// astro.config.mjs (never bundled into the production build — this file isn't
|
|
4
|
+
// imported by any Astro page/component, only served verbatim by that dev-only
|
|
5
|
+
// Vite middleware). Talks to /__tot/ai-edit and /__tot/ai-undo, which
|
|
6
|
+
// scripts/dev/ai-edit.mjs implements against the tenant this `tot dev` run is
|
|
7
|
+
// serving (TOT_DEV_TENANT).
|
|
8
|
+
//
|
|
9
|
+
// Plain vanilla JS, no build step, no framework — this never ships to users.
|
|
10
|
+
|
|
11
|
+
(function () {
|
|
12
|
+
const ROOT_ID = "__tot-ai-widget";
|
|
13
|
+
if (document.getElementById(ROOT_ID)) return; // one instance per page
|
|
14
|
+
|
|
15
|
+
const root = document.createElement("div");
|
|
16
|
+
root.id = ROOT_ID;
|
|
17
|
+
root.innerHTML = `
|
|
18
|
+
<style>
|
|
19
|
+
#${ROOT_ID} { position: fixed; right: 1rem; bottom: 1rem; z-index: 2147483647;
|
|
20
|
+
font: 14px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
21
|
+
#${ROOT_ID} .toggle { width: 3rem; height: 3rem; border-radius: 999px; border: none;
|
|
22
|
+
background: #16181d; color: #e8e6e0; font-size: 1.25rem; cursor: pointer;
|
|
23
|
+
box-shadow: 0 8px 24px -8px rgba(0,0,0,.45); }
|
|
24
|
+
#${ROOT_ID} .toggle:hover { background: #23262d; }
|
|
25
|
+
#${ROOT_ID} .panel { display: none; position: absolute; right: 0; bottom: 3.75rem;
|
|
26
|
+
width: 22rem; max-width: 90vw; background: #fff; color: #16191d; border-radius: 12px;
|
|
27
|
+
box-shadow: 0 16px 40px -12px rgba(0,0,0,.35); border: 1px solid #e4e0d6; padding: .9rem; }
|
|
28
|
+
#${ROOT_ID}.open .panel { display: block; }
|
|
29
|
+
#${ROOT_ID} textarea { width: 100%; box-sizing: border-box; resize: vertical; min-height: 3.5rem;
|
|
30
|
+
border: 1px solid #d8d4c8; border-radius: 8px; padding: .5rem .6rem; font: inherit; }
|
|
31
|
+
#${ROOT_ID} .row { display: flex; gap: .4rem; margin-top: .5rem; }
|
|
32
|
+
#${ROOT_ID} button.primary { flex: 1; background: #0f7b6c; color: #fff; border: none;
|
|
33
|
+
border-radius: 8px; padding: .5rem .7rem; font: 600 .85rem/1 inherit; cursor: pointer; }
|
|
34
|
+
#${ROOT_ID} button.primary:disabled { opacity: .5; cursor: default; }
|
|
35
|
+
#${ROOT_ID} button.secondary { background: #ece9e0; color: #2a2d31; border: none;
|
|
36
|
+
border-radius: 8px; padding: .5rem .7rem; font: 600 .8rem/1 inherit; cursor: pointer; }
|
|
37
|
+
#${ROOT_ID} .status { margin-top: .6rem; font-size: .82rem; white-space: pre-wrap; }
|
|
38
|
+
#${ROOT_ID} .status.error { color: #a83a3a; }
|
|
39
|
+
#${ROOT_ID} .status.ok { color: #0f7b6c; }
|
|
40
|
+
#${ROOT_ID} details { margin-top: .5rem; }
|
|
41
|
+
#${ROOT_ID} pre { max-height: 12rem; overflow: auto; background: #16181d; color: #dbe3ea;
|
|
42
|
+
border-radius: 8px; padding: .5rem .6rem; font-size: .75rem; }
|
|
43
|
+
/* Locate mode (C7): highlight box, hint, and result toast — top-level ids so
|
|
44
|
+
they overlay the whole page, outside the widget's own stacking. */
|
|
45
|
+
#${ROOT_ID}-locate-hi { position: fixed; z-index: 2147483646; pointer-events: none;
|
|
46
|
+
border: 2px solid #0f7b6c; background: rgba(15,123,108,.12); border-radius: 3px; display: none; }
|
|
47
|
+
#${ROOT_ID}-locate-hint { position: fixed; left: 50%; top: 1rem; transform: translateX(-50%);
|
|
48
|
+
z-index: 2147483647; background: #16181d; color: #e8e6e0; font: 600 .8rem/1 -apple-system, sans-serif;
|
|
49
|
+
padding: .55rem .9rem; border-radius: 999px; box-shadow: 0 8px 24px -8px rgba(0,0,0,.45); display: none; }
|
|
50
|
+
#${ROOT_ID}-locate-toast { position: fixed; left: 50%; bottom: 1rem; transform: translateX(-50%) translateY(160%);
|
|
51
|
+
z-index: 2147483647; background: #16181d; color: #e8e6e0; display: flex; align-items: center;
|
|
52
|
+
font: 500 .8rem/1.3 ui-monospace, Menlo, monospace; padding: .6rem 1rem; border-radius: 8px;
|
|
53
|
+
box-shadow: 0 8px 24px -8px rgba(0,0,0,.45); transition: transform .25s; max-width: 90vw; }
|
|
54
|
+
#${ROOT_ID}-locate-toast.show { transform: translateX(-50%) translateY(0); }
|
|
55
|
+
</style>
|
|
56
|
+
<button class="toggle" type="button" aria-label="Ask AI to edit this site" title="Ask AI to edit this site">✦</button>
|
|
57
|
+
<div class="panel">
|
|
58
|
+
<textarea placeholder="e.g. make it darker but still friendly" aria-label="Describe the change"></textarea>
|
|
59
|
+
<div class="row">
|
|
60
|
+
<button class="primary" type="button" data-action="ask">Ask</button>
|
|
61
|
+
<button class="secondary" type="button" data-action="undo">Undo last</button>
|
|
62
|
+
<button class="secondary" type="button" data-action="locate" title="Click an element on the page to jump to its source">◎ Locate</button>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="status" role="status" aria-live="polite"></div>
|
|
65
|
+
</div>
|
|
66
|
+
`;
|
|
67
|
+
document.body.appendChild(root);
|
|
68
|
+
|
|
69
|
+
const toggle = root.querySelector(".toggle");
|
|
70
|
+
const textarea = root.querySelector("textarea");
|
|
71
|
+
const askBtn = root.querySelector('[data-action="ask"]');
|
|
72
|
+
const undoBtn = root.querySelector('[data-action="undo"]');
|
|
73
|
+
const status = root.querySelector(".status");
|
|
74
|
+
|
|
75
|
+
toggle.addEventListener("click", () => root.classList.toggle("open"));
|
|
76
|
+
|
|
77
|
+
function setStatus(text, kind) {
|
|
78
|
+
status.textContent = text;
|
|
79
|
+
status.className = "status" + (kind ? " " + kind : "");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function renderResult(result) {
|
|
83
|
+
if (!result.ok) {
|
|
84
|
+
setStatus(result.message || `Refused (${result.status}).`, "error");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (result.status === "noop") {
|
|
88
|
+
setStatus(result.summary || "No change was needed.", "ok");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
setStatus(`✔ ${result.summary} — reloading…`, "ok");
|
|
92
|
+
// The file write already fires the existing content/theme HMR watch (see
|
|
93
|
+
// astro.config.mjs's tenantHotReload); nothing else to do here.
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function postJson(path, body) {
|
|
97
|
+
const res = await fetch(path, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: { "content-type": "application/json" },
|
|
100
|
+
body: JSON.stringify(body || {}),
|
|
101
|
+
});
|
|
102
|
+
return res.json();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
askBtn.addEventListener("click", async () => {
|
|
106
|
+
const prompt = textarea.value.trim();
|
|
107
|
+
if (!prompt) return;
|
|
108
|
+
askBtn.disabled = true;
|
|
109
|
+
setStatus("Asking…");
|
|
110
|
+
try {
|
|
111
|
+
const result = await postJson("/__tot/ai-edit", { prompt });
|
|
112
|
+
renderResult(result);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
setStatus(`Request failed: ${e.message}`, "error");
|
|
115
|
+
} finally {
|
|
116
|
+
askBtn.disabled = false;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
undoBtn.addEventListener("click", async () => {
|
|
121
|
+
undoBtn.disabled = true;
|
|
122
|
+
setStatus("Undoing…");
|
|
123
|
+
try {
|
|
124
|
+
const result = await postJson("/__tot/ai-undo", {});
|
|
125
|
+
if (!result.ok) {
|
|
126
|
+
setStatus(result.message || "Nothing to undo.", "error");
|
|
127
|
+
} else {
|
|
128
|
+
setStatus(`↺ Reverted "${result.undid}" — reloading…`, "ok");
|
|
129
|
+
}
|
|
130
|
+
} catch (e) {
|
|
131
|
+
setStatus(`Request failed: ${e.message}`, "error");
|
|
132
|
+
} finally {
|
|
133
|
+
undoBtn.disabled = false;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
textarea.addEventListener("keydown", (e) => {
|
|
138
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) askBtn.click();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ── Locate mode (C7): click an element → its source file:line:col ──────────
|
|
142
|
+
// Enter locate mode, hover to highlight, click an element: we capture a DOM path
|
|
143
|
+
// and POST it to /__tot/locate, which (server-side, scripts/dev/dev-locate.mjs)
|
|
144
|
+
// parses the raw-HTML tenant source with parse5 and walks the SAME path to a
|
|
145
|
+
// line:col. captureDomPath MUST stay in lockstep with scripts/dev/locate.mjs's
|
|
146
|
+
// walk (see its header + locate.test.mjs's round-trip): a DOM path is an array of
|
|
147
|
+
// { tag, index } from <body>'s child down to the target, index = 0-based position
|
|
148
|
+
// among same-tag element siblings (nth-of-type). Raw-HTML tenants only; the server
|
|
149
|
+
// refuses templated pages with { status: "not_raw_html" }.
|
|
150
|
+
function captureDomPath(el) {
|
|
151
|
+
const path = [];
|
|
152
|
+
let node = el;
|
|
153
|
+
while (node && node.tagName && node.tagName.toLowerCase() !== "body") {
|
|
154
|
+
const parent = node.parentElement;
|
|
155
|
+
if (!parent) break;
|
|
156
|
+
const tag = node.tagName.toLowerCase();
|
|
157
|
+
let index = 0;
|
|
158
|
+
for (let sib = node.previousElementSibling; sib; sib = sib.previousElementSibling) {
|
|
159
|
+
if (sib.tagName.toLowerCase() === tag) index += 1;
|
|
160
|
+
}
|
|
161
|
+
path.unshift({ tag, index });
|
|
162
|
+
if (parent.tagName && parent.tagName.toLowerCase() === "body") break;
|
|
163
|
+
node = parent;
|
|
164
|
+
}
|
|
165
|
+
return path;
|
|
166
|
+
}
|
|
167
|
+
// Exposed for the in-browser verification harness (dev-only, harmless in prod
|
|
168
|
+
// since this whole file is never bundled into the production build).
|
|
169
|
+
window.__totCaptureDomPath = captureDomPath;
|
|
170
|
+
|
|
171
|
+
const locateBtn = root.querySelector('[data-action="locate"]');
|
|
172
|
+
let hi, hint, ltoast, locating = false, lastHover = null;
|
|
173
|
+
|
|
174
|
+
function ensureLocateEls() {
|
|
175
|
+
if (hi) return;
|
|
176
|
+
hi = document.createElement("div");
|
|
177
|
+
hi.id = ROOT_ID + "-locate-hi";
|
|
178
|
+
hint = document.createElement("div");
|
|
179
|
+
hint.id = ROOT_ID + "-locate-hint";
|
|
180
|
+
hint.textContent = "Click an element to jump to its source · Esc to cancel";
|
|
181
|
+
ltoast = document.createElement("div");
|
|
182
|
+
ltoast.id = ROOT_ID + "-locate-toast";
|
|
183
|
+
document.body.append(hi, hint, ltoast);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function inWidget(el) {
|
|
187
|
+
return !!el && (el === root || root.contains(el) || el === hi || el === hint || el === ltoast);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function onMove(e) {
|
|
191
|
+
const el = document.elementFromPoint(e.clientX, e.clientY);
|
|
192
|
+
if (!el || inWidget(el)) {
|
|
193
|
+
hi.style.display = "none";
|
|
194
|
+
lastHover = null;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
lastHover = el;
|
|
198
|
+
const r = el.getBoundingClientRect();
|
|
199
|
+
hi.style.display = "block";
|
|
200
|
+
hi.style.left = r.left + "px";
|
|
201
|
+
hi.style.top = r.top + "px";
|
|
202
|
+
hi.style.width = r.width + "px";
|
|
203
|
+
hi.style.height = r.height + "px";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function showLToast(text, located) {
|
|
207
|
+
ensureLocateEls();
|
|
208
|
+
ltoast.textContent = text;
|
|
209
|
+
if (located) {
|
|
210
|
+
// Best-effort $EDITOR jump (server degrades gracefully when unset).
|
|
211
|
+
const open = document.createElement("button");
|
|
212
|
+
open.textContent = "Open in editor";
|
|
213
|
+
open.style.cssText =
|
|
214
|
+
"margin-left:.7rem;background:#0f7b6c;color:#fff;border:none;border-radius:6px;" +
|
|
215
|
+
"padding:.3rem .6rem;font:600 .72rem/1 -apple-system,sans-serif;cursor:pointer";
|
|
216
|
+
open.addEventListener("click", () => {
|
|
217
|
+
postJson("/__tot/open-in-editor", located).catch(() => {});
|
|
218
|
+
});
|
|
219
|
+
ltoast.appendChild(open);
|
|
220
|
+
}
|
|
221
|
+
ltoast.classList.add("show");
|
|
222
|
+
clearTimeout(showLToast._t);
|
|
223
|
+
showLToast._t = setTimeout(() => ltoast.classList.remove("show"), 6000);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function onClick(e) {
|
|
227
|
+
if (inWidget(e.target)) return; // clicks on the widget itself pass through
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
e.stopPropagation();
|
|
230
|
+
const el = lastHover || document.elementFromPoint(e.clientX, e.clientY);
|
|
231
|
+
exitLocate();
|
|
232
|
+
if (!el) return;
|
|
233
|
+
const domPath = captureDomPath(el);
|
|
234
|
+
// C8 (capstone) branches HERE: when framed (window !== window.parent) it
|
|
235
|
+
// postMessages { type:"tot:locate", path, line, col } to the parent editor
|
|
236
|
+
// instead of this standalone toast. C7 ships the standalone path only.
|
|
237
|
+
let res, body;
|
|
238
|
+
try {
|
|
239
|
+
res = await fetch("/__tot/locate", {
|
|
240
|
+
method: "POST",
|
|
241
|
+
headers: { "content-type": "application/json" },
|
|
242
|
+
body: JSON.stringify({ domPath, path: location.pathname }),
|
|
243
|
+
});
|
|
244
|
+
body = await res.json();
|
|
245
|
+
} catch (err) {
|
|
246
|
+
showLToast("Locate unavailable — is tot dev running?");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
// C8 capstone: when this page is FRAMED (the live-preview iframe inside /dev),
|
|
250
|
+
// deliver the result to the parent cockpit instead of a standalone toast — the
|
|
251
|
+
// parent opens the file + places the cursor. Only content-region hits (a real
|
|
252
|
+
// {line,col}) are forwarded; chrome/refused clicks stay silent inside the frame
|
|
253
|
+
// (no toast over the preview, no misleading jump), honoring the content-region
|
|
254
|
+
// decision. targetOrigin is our own origin (same-origin preview) — never "*".
|
|
255
|
+
const framed = window !== window.parent;
|
|
256
|
+
if (!res.ok || (body && body.status)) {
|
|
257
|
+
if (framed) return; // refused inside the frame — quietly no-op
|
|
258
|
+
showLToast(
|
|
259
|
+
body && body.status === "not_raw_html"
|
|
260
|
+
? "Locate works on raw-HTML tenants only — this page is templated."
|
|
261
|
+
: "Couldn’t find that element in the source.",
|
|
262
|
+
);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (framed) {
|
|
266
|
+
try {
|
|
267
|
+
window.parent.postMessage(
|
|
268
|
+
{ type: "tot:locate", path: body.path, line: body.line, col: body.col },
|
|
269
|
+
window.location.origin,
|
|
270
|
+
);
|
|
271
|
+
} catch (err) { /* parent gone / cross-origin — nothing to do */ }
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
showLToast(`${body.path}:${body.line}:${body.col}`, body);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function onEsc(e) {
|
|
278
|
+
if (e.key === "Escape") {
|
|
279
|
+
e.preventDefault();
|
|
280
|
+
exitLocate();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function enterLocate() {
|
|
285
|
+
ensureLocateEls();
|
|
286
|
+
locating = true;
|
|
287
|
+
root.classList.remove("open");
|
|
288
|
+
hint.style.display = "block";
|
|
289
|
+
document.addEventListener("mousemove", onMove, true);
|
|
290
|
+
document.addEventListener("click", onClick, true);
|
|
291
|
+
document.addEventListener("keydown", onEsc, true);
|
|
292
|
+
document.body.style.cursor = "crosshair";
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function exitLocate() {
|
|
296
|
+
locating = false;
|
|
297
|
+
if (hi) hi.style.display = "none";
|
|
298
|
+
if (hint) hint.style.display = "none";
|
|
299
|
+
document.removeEventListener("mousemove", onMove, true);
|
|
300
|
+
document.removeEventListener("click", onClick, true);
|
|
301
|
+
document.removeEventListener("keydown", onEsc, true);
|
|
302
|
+
document.body.style.cursor = "";
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (locateBtn) {
|
|
306
|
+
locateBtn.addEventListener("click", () => (locating ? exitLocate() : enterLocate()));
|
|
307
|
+
}
|
|
308
|
+
})();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment-aware endpoint base for the /dev cockpit (file tree/viewer/diff +
|
|
3
|
+
* click-to-source). Local `astro dev` serves these off dev-server-only plugins at
|
|
4
|
+
* `/__tot/*` (dev-plugins/dev-file-browser.mjs, dev-write.mjs, dev-diff.mjs, ...) —
|
|
5
|
+
* paths that don't exist in the deployed Cloudflare Worker. The hosted Worker instead
|
|
6
|
+
* serves the SAME shape off `/api/dev/*` Astro API routes, landing incrementally
|
|
7
|
+
* (cp-tree, cp-file, cp-write, cp-diff, cp-locate). One dev.astro UI, one resolver.
|
|
8
|
+
*
|
|
9
|
+
* See decision `architecture-environment-aware-cockpit`
|
|
10
|
+
* (workstream tot-lime-cloud-dev-cockpit).
|
|
11
|
+
*/
|
|
12
|
+
export function resolveDevApiBase(isDev: boolean): string {
|
|
13
|
+
return isDev ? "/__tot" : "/api/dev";
|
|
14
|
+
}
|