@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,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/content-edit/ai-edit — the `/dev` page's "Build with Claude" live loop
|
|
3
|
+
* (WS5 G4 + decision draft-overlay-same-origin). A prompt becomes a reviewable
|
|
4
|
+
* per-file diff via tot-mcp's `content_edit` tool, applied to the signed-in
|
|
5
|
+
* developer's OWN tenant content — and materialized into their private DRAFT
|
|
6
|
+
* overlay so the /dev live preview shows the change instantly (visible AND
|
|
7
|
+
* interactive), while live/test channels and every other viewer are untouched.
|
|
8
|
+
*
|
|
9
|
+
* AUTH: `/dev`'s own signed-in-viewer session (`readViewerSession`) is the
|
|
10
|
+
* developer-level gate; the MCP call authenticates as the storefront's own
|
|
11
|
+
* TENANT-OPERATOR credential (`getContentEditClient` — the same
|
|
12
|
+
* credential_validate path `/api/changes/accept` uses). The tenant edited is the
|
|
13
|
+
* SESSION'S OWN first membership — never a caller-supplied tenant.
|
|
14
|
+
*
|
|
15
|
+
* DRAFT MATERIALIZATION: content_edit returns unified DIFFS, not contents, so we
|
|
16
|
+
* re-apply each diff to the exact content we sent (applyUnifiedDiff, strict +
|
|
17
|
+
* fail-closed) and write the results to `dev-draft:` KV keys with a TTL, minting
|
|
18
|
+
* the signed tot_dev_draft cookie the middleware's overlay verifies. When KV or
|
|
19
|
+
* the signing secret is absent (e.g. bare `astro dev`), the edit still runs and
|
|
20
|
+
* returns diffs — `applied:false` tells the page the preview won't change.
|
|
21
|
+
*/
|
|
22
|
+
import type { APIContext } from "astro";
|
|
23
|
+
import { readViewerSession } from "@/lib/auth/route";
|
|
24
|
+
import { getContentEditClient } from "@/lib/content-edit";
|
|
25
|
+
import { applyUnifiedDiff, DiffApplyError } from "@/lib/content-edit/applyDiff";
|
|
26
|
+
import { json } from "@/lib/grants/route.js";
|
|
27
|
+
import { readEnv, readKv } from "@/lib/env";
|
|
28
|
+
// Arbitrary-tenant lookup lives behind the config-layer provider so this handler
|
|
29
|
+
// doesn't import the concrete resolver/static-map directly (tenancy-resolver-fence).
|
|
30
|
+
import { resolvePreviewTenant } from "@/config/previewTenantLookup";
|
|
31
|
+
import { createContentProvider } from "@/lib/storyblok/provider";
|
|
32
|
+
import { readPublishedArtifact, themeArtifactPath } from "@tot/public-runtime";
|
|
33
|
+
import {
|
|
34
|
+
DEV_DRAFT_COOKIE,
|
|
35
|
+
DEV_DRAFT_TTL_SECONDS,
|
|
36
|
+
devDraftKey,
|
|
37
|
+
signDevDraftToken,
|
|
38
|
+
verifyDevDraftToken,
|
|
39
|
+
} from "@/lib/dev/devDraft";
|
|
40
|
+
import type { ContentEditFile } from "@/lib/content-edit/types";
|
|
41
|
+
|
|
42
|
+
export const prerender = false;
|
|
43
|
+
|
|
44
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
45
|
+
const session = await readViewerSession({ cookies: context.cookies });
|
|
46
|
+
if (!session) return json({ error: "sign in required" }, 401);
|
|
47
|
+
if (!session.capability || session.capability === "none") {
|
|
48
|
+
return json({ error: "no developer capability on this account" }, 403);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let body: { prompt?: unknown };
|
|
52
|
+
try {
|
|
53
|
+
body = (await context.request.json()) as typeof body;
|
|
54
|
+
} catch {
|
|
55
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
56
|
+
}
|
|
57
|
+
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
|
58
|
+
if (!prompt) return json({ error: "prompt is required" }, 400);
|
|
59
|
+
|
|
60
|
+
// The tenant is the session's OWN store (first membership, else the session's
|
|
61
|
+
// primary resource) — the same resolution /dev renders with. No caller-supplied
|
|
62
|
+
// tenant is ever honored here.
|
|
63
|
+
const wanted = (session.tenants?.[0]?.resource ?? session.resource ?? "").toLowerCase();
|
|
64
|
+
const tenant = wanted ? await resolvePreviewTenant(wanted) : null;
|
|
65
|
+
if (!tenant) return json({ error: "no store linked to this account" }, 403);
|
|
66
|
+
|
|
67
|
+
const kv = await readKv("TENANT_CACHE");
|
|
68
|
+
const kvGet = kv ? (key: string) => kv.get(key) : async () => null;
|
|
69
|
+
const secret = (await readEnv("CUSTOMIZATION_PREVIEW_SECRET")) ?? null;
|
|
70
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
71
|
+
|
|
72
|
+
// Reuse the caller's existing draft (so successive edits stack) or start a new one.
|
|
73
|
+
const priorCookie = context.cookies.get(DEV_DRAFT_COOKIE)?.value ?? null;
|
|
74
|
+
const priorClaims =
|
|
75
|
+
priorCookie && secret ? await verifyDevDraftToken(secret, priorCookie, nowSeconds) : null;
|
|
76
|
+
const draftId =
|
|
77
|
+
priorClaims?.tenantId === tenant.tenant_id ? priorClaims.draftId : crypto.randomUUID();
|
|
78
|
+
|
|
79
|
+
// Current content = the developer's draft when present, else what actually serves
|
|
80
|
+
// (published overlay → build-time), so edits stack and always start from reality.
|
|
81
|
+
const provider = createContentProvider(tenant, kv ? { get: kvGet, env: "live" } : undefined);
|
|
82
|
+
const draftHome = await kvGet(devDraftKey(tenant.tenant_id, draftId, "content/home.html"));
|
|
83
|
+
const homeHtml = draftHome ?? (await provider.getHomeHtml());
|
|
84
|
+
const draftTheme = await kvGet(devDraftKey(tenant.tenant_id, draftId, "theme.json"));
|
|
85
|
+
const themeJson =
|
|
86
|
+
draftTheme ??
|
|
87
|
+
(await readPublishedArtifact(kvGet, tenant.tenant_id, "live", themeArtifactPath(tenant.tenant_id))) ??
|
|
88
|
+
JSON.stringify(tenant.theme_tokens ?? {}, null, 2);
|
|
89
|
+
|
|
90
|
+
// content_edit's allowlist: home document + theme. A block-based tenant with no raw
|
|
91
|
+
// home.html still gets theme edits (themeStyle drives its pages).
|
|
92
|
+
const files: ContentEditFile[] = [];
|
|
93
|
+
if (homeHtml) files.push({ path: "content/home.html", content: homeHtml });
|
|
94
|
+
files.push({ path: "theme.json", content: themeJson });
|
|
95
|
+
|
|
96
|
+
const client = await getContentEditClient();
|
|
97
|
+
if (client instanceof Response) return client;
|
|
98
|
+
|
|
99
|
+
let result;
|
|
100
|
+
try {
|
|
101
|
+
result = await client.editContent(prompt, files);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
104
|
+
return json({ ok: false, error: message }, 502);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Materialize the draft: re-apply each returned diff to the exact content we sent.
|
|
108
|
+
// Strict + fail-closed per file — a bad diff writes nothing for that file. Theme
|
|
109
|
+
// must still parse as JSON before it may land (a broken draft theme would degrade
|
|
110
|
+
// the whole preview).
|
|
111
|
+
const sent = new Map(files.map((f) => [f.path, f.content]));
|
|
112
|
+
const appliedPaths: string[] = [];
|
|
113
|
+
const canApply = !!kv && !!secret;
|
|
114
|
+
if (canApply) {
|
|
115
|
+
for (const d of result.diffs ?? []) {
|
|
116
|
+
const base = sent.get(d.path);
|
|
117
|
+
if (base == null) continue; // never write a path we didn't offer
|
|
118
|
+
let next: string;
|
|
119
|
+
try {
|
|
120
|
+
next = applyUnifiedDiff(base, d.diff);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (err instanceof DiffApplyError) continue;
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
if (next === base) continue;
|
|
126
|
+
if (d.path === "theme.json") {
|
|
127
|
+
try {
|
|
128
|
+
JSON.parse(next);
|
|
129
|
+
} catch {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
await kv!.put(devDraftKey(tenant.tenant_id, draftId, d.path), next, {
|
|
134
|
+
expirationTtl: DEV_DRAFT_TTL_SECONDS,
|
|
135
|
+
});
|
|
136
|
+
appliedPaths.push(d.path);
|
|
137
|
+
}
|
|
138
|
+
if (appliedPaths.length > 0) {
|
|
139
|
+
// (Re)issue the signed draft cookie so the middleware overlay serves this draft.
|
|
140
|
+
const token = await signDevDraftToken(
|
|
141
|
+
secret!,
|
|
142
|
+
tenant.tenant_id,
|
|
143
|
+
draftId,
|
|
144
|
+
DEV_DRAFT_TTL_SECONDS,
|
|
145
|
+
nowSeconds,
|
|
146
|
+
);
|
|
147
|
+
context.cookies.set(DEV_DRAFT_COOKIE, token, {
|
|
148
|
+
httpOnly: true,
|
|
149
|
+
secure: true,
|
|
150
|
+
sameSite: "lax",
|
|
151
|
+
path: "/",
|
|
152
|
+
maxAge: DEV_DRAFT_TTL_SECONDS,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return json({
|
|
158
|
+
ok: true,
|
|
159
|
+
...result,
|
|
160
|
+
// True when the draft landed and the live preview will show it on reload.
|
|
161
|
+
applied: appliedPaths.length > 0,
|
|
162
|
+
appliedPaths,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSP violation collector (enables the report-only → enforce
|
|
3
|
+
* rollout). Browsers POST violation reports here (via `report-to` /
|
|
4
|
+
* `report-uri`); we log them so platform + tenant-script CSP violations surface
|
|
5
|
+
* before we flip the policy from Report-Only to enforcing. Returns 204.
|
|
6
|
+
*
|
|
7
|
+
* Phase 1: log to the worker console. A later iteration can forward to an
|
|
8
|
+
* analytics/observability sink.
|
|
9
|
+
*/
|
|
10
|
+
import type { APIRoute } from "astro";
|
|
11
|
+
|
|
12
|
+
export const prerender = false;
|
|
13
|
+
|
|
14
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
15
|
+
try {
|
|
16
|
+
const body = await request.text();
|
|
17
|
+
if (body) {
|
|
18
|
+
// Reports can be `application/csp-report` or `application/reports+json`.
|
|
19
|
+
console.warn("[csp-report]", body.slice(0, 4000));
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
/* never fail a report submission */
|
|
23
|
+
}
|
|
24
|
+
return new Response(null, { status: 204 });
|
|
25
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/dashboard/enter-vendor { vendorAppDomain } — the ToT-STAFF admission
|
|
3
|
+
* endpoint (#8425 staff access). Phase 2 of two-phase admission: on the staff
|
|
4
|
+
* viewer explicitly selecting a searched vendor, tot20 writes the server-side
|
|
5
|
+
* AUDIT row and returns the staffRoles-scoped capability; we then stamp a
|
|
6
|
+
* short-lived {@link StaffSelection} on the session and admit the viewer to
|
|
7
|
+
* `/dashboard/<appDomain>` scoped to that capability.
|
|
8
|
+
*
|
|
9
|
+
* AUTHORIZATION — two independent axes:
|
|
10
|
+
* NEAR (this endpoint): a valid `tot_session` carrying ToT-STAFF scope
|
|
11
|
+
* (`staff[]` non-empty). No session → 401; no staff scope → 403.
|
|
12
|
+
* FAR (tot20): `/clients/staffVendorAccess`, operator-authed as storefront,
|
|
13
|
+
* presenting a storefront-signed staff broker assertion in the
|
|
14
|
+
* `X-ToT-Broker-Assertion` header (minted from the verified session email).
|
|
15
|
+
* tot20 verifies it, re-checks staff scope from its `sub`, scopes the capability,
|
|
16
|
+
* and writes the audit row — it is the authority. No plaintext email on the wire.
|
|
17
|
+
*
|
|
18
|
+
* The staff selection is a DISTINCT access path from `tenants[]`: we NEVER
|
|
19
|
+
* synthesize a membership. The dashboard guard + login gate consult the marker
|
|
20
|
+
* separately (see tenantSelection.staffAdmits / loginGate.sessionAdmitsHost).
|
|
21
|
+
*/
|
|
22
|
+
import type { APIContext } from "astro";
|
|
23
|
+
import { readViewerSession, getSessionMutationHandle } from "@/lib/auth/route";
|
|
24
|
+
import { updateSession } from "@/lib/auth/session";
|
|
25
|
+
import { isStaffSession, buildStaffSelection } from "@/lib/dashboard/staffAdmission";
|
|
26
|
+
import {
|
|
27
|
+
createStaffVendorAccessClient,
|
|
28
|
+
staffAccessHttpStatus,
|
|
29
|
+
StaffVendorAccessClientError,
|
|
30
|
+
} from "@/lib/dashboard/staffVendorAccess";
|
|
31
|
+
import { mintBrokerAssertion, BrokerAssertionError } from "@/lib/auth/brokerAssertion";
|
|
32
|
+
|
|
33
|
+
export const prerender = false;
|
|
34
|
+
|
|
35
|
+
/** JSON response with no-store (staff access state must never be edge-cached). */
|
|
36
|
+
function json(body: unknown, status = 200): Response {
|
|
37
|
+
return new Response(JSON.stringify(body), {
|
|
38
|
+
status,
|
|
39
|
+
headers: { "content-type": "application/json", "cache-control": "no-store" },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
44
|
+
const session = await readViewerSession(context);
|
|
45
|
+
if (!session) return json({ error: "not signed in" }, 401);
|
|
46
|
+
if (!isStaffSession(session)) {
|
|
47
|
+
return json({ error: "staff access required", reason: "notStaff" }, 403);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let body: { vendorAppDomain?: unknown };
|
|
51
|
+
try {
|
|
52
|
+
body = (await context.request.json()) as typeof body;
|
|
53
|
+
} catch {
|
|
54
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
55
|
+
}
|
|
56
|
+
const vendorAppDomain =
|
|
57
|
+
typeof body.vendorAppDomain === "string" ? body.vendorAppDomain.trim() : "";
|
|
58
|
+
if (!vendorAppDomain) return json({ error: "vendorAppDomain is required" }, 400);
|
|
59
|
+
|
|
60
|
+
let client;
|
|
61
|
+
try {
|
|
62
|
+
client = await createStaffVendorAccessClient();
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (err instanceof StaffVendorAccessClientError) {
|
|
65
|
+
return json({ error: "staff-access integration not configured" }, 503);
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// #8453 `staff-invite-proof-via-broker`: prove the staff actor with a fresh
|
|
71
|
+
// storefront-signed assertion minted from the VERIFIED session email. Fail closed
|
|
72
|
+
// (503) if the signing key is unconfigured.
|
|
73
|
+
let brokerAssertion: string;
|
|
74
|
+
try {
|
|
75
|
+
brokerAssertion = await mintBrokerAssertion(session.email);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err instanceof BrokerAssertionError) {
|
|
78
|
+
return json({ error: "staff-proof signing not configured" }, 503);
|
|
79
|
+
}
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// FAR authority: tot20 verifies the assertion, derives the staff actor, scopes the
|
|
84
|
+
// capability, and writes the audit row. We admit ONLY on its ok.
|
|
85
|
+
const result = await client.grantStaffAccess(brokerAssertion, vendorAppDomain);
|
|
86
|
+
if (!result.ok) {
|
|
87
|
+
return json(
|
|
88
|
+
{ error: result.message, reason: result.reason, code: result.code },
|
|
89
|
+
staffAccessHttpStatus(result.reason, result.status),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Stamp the distinct staff-selection marker on the session (short-lived, scoped
|
|
94
|
+
// to the returned capability). Never a tenants[] entry.
|
|
95
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
96
|
+
const selection = buildStaffSelection({
|
|
97
|
+
resource: vendorAppDomain,
|
|
98
|
+
capability: result.capability,
|
|
99
|
+
nowSeconds,
|
|
100
|
+
});
|
|
101
|
+
const handle = await getSessionMutationHandle(context);
|
|
102
|
+
if (!handle) {
|
|
103
|
+
// No SESSION KV / no cookie (e.g. plain `astro dev`) — the audit row is
|
|
104
|
+
// written but we can't persist the marker. Fail closed so the viewer isn't
|
|
105
|
+
// told they're admitted when the guard can't see it.
|
|
106
|
+
return json({ error: "session store unavailable; cannot persist admission" }, 503);
|
|
107
|
+
}
|
|
108
|
+
const updated = await updateSession(
|
|
109
|
+
handle.store,
|
|
110
|
+
handle.id,
|
|
111
|
+
(rec) => ({ ...rec, staffSelection: selection }),
|
|
112
|
+
nowSeconds,
|
|
113
|
+
);
|
|
114
|
+
if (!updated) {
|
|
115
|
+
return json({ error: "session expired; sign in again" }, 401);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return json({
|
|
119
|
+
ok: true,
|
|
120
|
+
appDomain: vendorAppDomain,
|
|
121
|
+
capability: result.capability,
|
|
122
|
+
redirect: `/dashboard/${encodeURIComponent(vendorAppDomain)}`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET|POST /api/dashboard/vendor-search?q=<prefix> — the ToT-STAFF vendor search
|
|
3
|
+
* proxy (#8425 staff access). Phase 1 of two-phase staff admission: a staff viewer
|
|
4
|
+
* finds a vendor by prefix; there is no silent access to every host.
|
|
5
|
+
*
|
|
6
|
+
* AUTHORIZATION — two independent axes:
|
|
7
|
+
* NEAR (this endpoint): a valid `tot_session` that carries ToT-STAFF scope
|
|
8
|
+
* (`staff[]` non-empty). No session → 401; a session without staff scope →
|
|
9
|
+
* 403 (a plain member has nothing to search across — they use their picker).
|
|
10
|
+
* FAR (tot20): storefront calls `/clients/vendorSearch` as its OWN operator
|
|
11
|
+
* identity, presenting a storefront-signed staff broker assertion in the
|
|
12
|
+
* `X-ToT-Broker-Assertion` header (minted from the verified session email);
|
|
13
|
+
* tot20 verifies it, derives the staff actor from its `sub`, and scopes results
|
|
14
|
+
* to that user's staffRoles. No spoofable plaintext email crosses the wire.
|
|
15
|
+
*
|
|
16
|
+
* GET reads `q` (+ optional `limit`) from the query; POST reads them from a JSON
|
|
17
|
+
* body — the autocomplete client uses GET, but POST is accepted so a no-JS/form
|
|
18
|
+
* path works too. Returns `{ vendors: [{ appDomain, displayName }] }`.
|
|
19
|
+
*/
|
|
20
|
+
import type { APIContext } from "astro";
|
|
21
|
+
import { readViewerSession } from "@/lib/auth/route";
|
|
22
|
+
import { isStaffSession } from "@/lib/dashboard/staffAdmission";
|
|
23
|
+
import {
|
|
24
|
+
createStaffVendorAccessClient,
|
|
25
|
+
staffAccessHttpStatus,
|
|
26
|
+
StaffVendorAccessClientError,
|
|
27
|
+
} from "@/lib/dashboard/staffVendorAccess";
|
|
28
|
+
import { mintBrokerAssertion, BrokerAssertionError } from "@/lib/auth/brokerAssertion";
|
|
29
|
+
import {
|
|
30
|
+
parseVendorQuery,
|
|
31
|
+
ANCHORED_FETCH_LIMIT,
|
|
32
|
+
ANCHORED_DISPLAY_LIMIT,
|
|
33
|
+
} from "@/lib/dashboard/vendorQuery";
|
|
34
|
+
|
|
35
|
+
export const prerender = false;
|
|
36
|
+
|
|
37
|
+
/** JSON response with no-store (staff access state must never be edge-cached). */
|
|
38
|
+
function json(body: unknown, status = 200): Response {
|
|
39
|
+
return new Response(JSON.stringify(body), {
|
|
40
|
+
status,
|
|
41
|
+
headers: { "content-type": "application/json", "cache-control": "no-store" },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function handle(context: APIContext, q: string, limit: number | undefined): Promise<Response> {
|
|
46
|
+
const session = await readViewerSession(context);
|
|
47
|
+
if (!session) return json({ error: "not signed in" }, 401);
|
|
48
|
+
if (!isStaffSession(session)) {
|
|
49
|
+
return json({ error: "staff access required", reason: "notStaff" }, 403);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let client;
|
|
53
|
+
try {
|
|
54
|
+
client = await createStaffVendorAccessClient();
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (err instanceof StaffVendorAccessClientError) {
|
|
57
|
+
return json({ error: "staff-access integration not configured" }, 503);
|
|
58
|
+
}
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// #8453 `staff-invite-proof-via-broker`: prove the staff actor with a fresh
|
|
63
|
+
// storefront-signed assertion minted from the VERIFIED session email — tot20
|
|
64
|
+
// derives the actor + scopes results from it. Fail closed (503) if unconfigured.
|
|
65
|
+
let brokerAssertion: string;
|
|
66
|
+
try {
|
|
67
|
+
brokerAssertion = await mintBrokerAssertion(session.email);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (err instanceof BrokerAssertionError) {
|
|
70
|
+
return json({ error: "staff-proof signing not configured" }, 503);
|
|
71
|
+
}
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Anchor syntax (^prefix / suffix$ / ^exact$) narrows tot20's substring search.
|
|
76
|
+
// Send the anchor-stripped CORE term upstream (with a widened cap so the exact hit
|
|
77
|
+
// is in the set), then post-filter by the anchor against appDomain. A plain query
|
|
78
|
+
// is unchanged: sent as-is, no post-filter. See @/lib/dashboard/vendorQuery.
|
|
79
|
+
const parsed = parseVendorQuery(q);
|
|
80
|
+
const upstreamQuery = parsed.anchored ? parsed.core : q;
|
|
81
|
+
const upstreamLimit = parsed.anchored ? ANCHORED_FETCH_LIMIT : limit;
|
|
82
|
+
|
|
83
|
+
const result = await client.searchVendors(brokerAssertion, upstreamQuery, upstreamLimit);
|
|
84
|
+
if (!result.ok) {
|
|
85
|
+
return json(
|
|
86
|
+
{ error: result.message, reason: result.reason, code: result.code },
|
|
87
|
+
staffAccessHttpStatus(result.reason, result.status),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const vendors = parsed.anchored
|
|
91
|
+
? result.vendors
|
|
92
|
+
.filter((v) => parsed.matches(v.appDomain))
|
|
93
|
+
.slice(0, limit && limit > 0 ? limit : ANCHORED_DISPLAY_LIMIT)
|
|
94
|
+
: result.vendors;
|
|
95
|
+
return json({ vendors });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseLimit(raw: string | null | undefined): number | undefined {
|
|
99
|
+
if (raw == null || raw === "") return undefined;
|
|
100
|
+
const n = Number(raw);
|
|
101
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
105
|
+
const url = new URL(context.request.url);
|
|
106
|
+
const q = (url.searchParams.get("q") ?? "").trim();
|
|
107
|
+
return handle(context, q, parseLimit(url.searchParams.get("limit")));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
111
|
+
let body: { q?: unknown; limit?: unknown };
|
|
112
|
+
try {
|
|
113
|
+
body = (await context.request.json()) as typeof body;
|
|
114
|
+
} catch {
|
|
115
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
116
|
+
}
|
|
117
|
+
const q = typeof body.q === "string" ? body.q.trim() : "";
|
|
118
|
+
const limit = typeof body.limit === "number" ? body.limit : undefined;
|
|
119
|
+
return handle(context, q, limit);
|
|
120
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET/POST /api/dev/activity — the hosted /dev panel's file-level activity
|
|
3
|
+
* feed (local→hosted activity bridge). See handoff
|
|
4
|
+
* 2026-07-14-hosted-dev-activity-bridge-and-stepper.
|
|
5
|
+
*
|
|
6
|
+
* GET: the signed-in developer's own browser polls this (same capability gate
|
|
7
|
+
* as every other /dev route) to render the activity log and feed the Step-2
|
|
8
|
+
* gate's completion signal. POLL, not a held-open stream — Cloudflare Workers
|
|
9
|
+
* are stateless per request, so a poll means a Worker redeploy mid-session
|
|
10
|
+
* needs no reconnect logic on either side.
|
|
11
|
+
*
|
|
12
|
+
* POST: the developer's LOCAL dev-loop process reports one file save,
|
|
13
|
+
* authorized by the opaque bearer minted alongside their cli-signin-code
|
|
14
|
+
* paste (lib/dev/activityIngestToken.ts) — NOT the browser session cookie,
|
|
15
|
+
* since the local process has no cookie.
|
|
16
|
+
*/
|
|
17
|
+
import type { APIContext } from "astro";
|
|
18
|
+
import { readViewerSession } from "@/lib/auth/route";
|
|
19
|
+
import { readKv } from "@/lib/env";
|
|
20
|
+
import { resolvePreviewTenant } from "@/config/previewTenantLookup";
|
|
21
|
+
import { verifyActivityIngestToken } from "@/lib/dev/activityIngestToken";
|
|
22
|
+
import {
|
|
23
|
+
appendActivity,
|
|
24
|
+
readActivity,
|
|
25
|
+
describeActivity,
|
|
26
|
+
type ActivityEvent,
|
|
27
|
+
type ActivityEventKind,
|
|
28
|
+
} from "@/lib/dev/activityStore";
|
|
29
|
+
import { readRuntime, writeRuntime, shapeRuntime, toRuntimeView } from "@/lib/dev/runtimeStore";
|
|
30
|
+
|
|
31
|
+
export const prerender = false;
|
|
32
|
+
|
|
33
|
+
function json(body: unknown, status = 200): Response {
|
|
34
|
+
return new Response(JSON.stringify(body), {
|
|
35
|
+
status,
|
|
36
|
+
headers: { "content-type": "application/json" },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
41
|
+
const session = await readViewerSession({ cookies: context.cookies });
|
|
42
|
+
if (!session) return json({ error: "sign in required" }, 401);
|
|
43
|
+
if (!session.capability || session.capability === "none") {
|
|
44
|
+
return json({ error: "no developer capability on this account" }, 403);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Same tenant resolution every other /dev route uses — the session's own store,
|
|
48
|
+
// never caller input.
|
|
49
|
+
const wanted = (session.tenants?.[0]?.resource ?? session.resource ?? "").toLowerCase();
|
|
50
|
+
const tenant = wanted ? await resolvePreviewTenant(wanted) : null;
|
|
51
|
+
if (!tenant) return json({ error: "no store linked to this account" }, 403);
|
|
52
|
+
|
|
53
|
+
const kv = await readKv("TENANT_CACHE");
|
|
54
|
+
const events = kv ? await readActivity(kv, tenant.tenant_id) : [];
|
|
55
|
+
// The LIVE local-dev runtime (latest heartbeat), separate from the file-save
|
|
56
|
+
// feed — the cockpit renders it as a clickable local link + CLI version, with
|
|
57
|
+
// `live` computed server-side so a skewed local clock can't fake liveness.
|
|
58
|
+
const runtime = kv ? await readRuntime(kv, tenant.tenant_id) : null;
|
|
59
|
+
return json({
|
|
60
|
+
events: events.map((e) => ({ ...e, description: describeActivity(e.file, e.event) })),
|
|
61
|
+
runtime: runtime ? toRuntimeView(runtime) : null,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
66
|
+
const auth = context.request.headers.get("authorization") || "";
|
|
67
|
+
const m = /^Bearer\s+(.+)$/i.exec(auth);
|
|
68
|
+
if (!m) return json({ error: "missing bearer token" }, 401);
|
|
69
|
+
|
|
70
|
+
const sessionKv = await readKv("SESSION");
|
|
71
|
+
if (!sessionKv) return json({ error: "activity bridge not configured" }, 503);
|
|
72
|
+
const claims = await verifyActivityIngestToken(sessionKv, m[1] ?? "");
|
|
73
|
+
if (!claims) return json({ error: "invalid or expired token" }, 401);
|
|
74
|
+
|
|
75
|
+
let body: {
|
|
76
|
+
file?: unknown;
|
|
77
|
+
event?: unknown;
|
|
78
|
+
at?: unknown;
|
|
79
|
+
cliVersion?: unknown;
|
|
80
|
+
runnerVersion?: unknown;
|
|
81
|
+
url?: unknown;
|
|
82
|
+
};
|
|
83
|
+
try {
|
|
84
|
+
body = (await context.request.json()) as typeof body;
|
|
85
|
+
} catch {
|
|
86
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The token's own bound tenant, never a caller-supplied one — a stolen/expired
|
|
90
|
+
// token can't be redirected at a different vendor's runtime/activity feed.
|
|
91
|
+
const tenant = await resolvePreviewTenant(claims.tenant.toLowerCase());
|
|
92
|
+
if (!tenant) return json({ error: "no store linked to this token" }, 403);
|
|
93
|
+
const tenantCache = await readKv("TENANT_CACHE");
|
|
94
|
+
|
|
95
|
+
// Heartbeat: latest-wins local-dev runtime (CLI version + live localhost URL),
|
|
96
|
+
// stored SEPARATELY from the file-save ring buffer. Not a file event, so it
|
|
97
|
+
// takes no `file`; liveness is stamped from the SERVER clock (aliveAt), never
|
|
98
|
+
// the client's `at`.
|
|
99
|
+
if (body.event === "heartbeat") {
|
|
100
|
+
if (!tenantCache) return json({ ok: true, stored: false }); // no KV in this runtime (dev)
|
|
101
|
+
await writeRuntime(tenantCache, tenant.tenant_id, shapeRuntime(body, Date.now()));
|
|
102
|
+
return json({ ok: true, stored: true, kind: "runtime" });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const isUp = body.event === "up";
|
|
106
|
+
const file = typeof body.file === "string" ? body.file : "";
|
|
107
|
+
if (!file && !isUp) return json({ error: "file is required" }, 400);
|
|
108
|
+
// A file-relative path never needs to be long; cap it so a misbehaving or
|
|
109
|
+
// hostile bridge-token holder can't inflate the per-tenant KV ring-buffer
|
|
110
|
+
// entry toward KV's per-value size limit.
|
|
111
|
+
if (file.length > 512) return json({ error: "file path too long" }, 400);
|
|
112
|
+
const event: ActivityEventKind = isUp
|
|
113
|
+
? "up"
|
|
114
|
+
: body.event === "add" || body.event === "unlink"
|
|
115
|
+
? body.event
|
|
116
|
+
: "change";
|
|
117
|
+
const at = typeof body.at === "number" && Number.isFinite(body.at) ? body.at : Date.now();
|
|
118
|
+
|
|
119
|
+
if (!tenantCache) return json({ ok: true, stored: false }); // no KV in this runtime (dev) — accepted, not stored
|
|
120
|
+
|
|
121
|
+
const ev: ActivityEvent = { file, event, at };
|
|
122
|
+
await appendActivity(tenantCache, tenant.tenant_id, ev);
|
|
123
|
+
return json({ ok: true, stored: true });
|
|
124
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/dev/cli-signin-code — mint a browserless CLI sign-in command for the
|
|
3
|
+
* SIGNED-IN developer (DevAccess #8425). The /dev getting-started page calls this to
|
|
4
|
+
* render a copyable `tot login --code <token>` so the developer's terminal signs in with
|
|
5
|
+
* no second browser round-trip and no OTP.
|
|
6
|
+
*
|
|
7
|
+
* AUTHORIZATION: the viewer's own session (the `tot_session` cookie). 401 if not signed
|
|
8
|
+
* in, 403 if the session carries no developer capability (nothing to sign a CLI into).
|
|
9
|
+
* The minted-for email is the VERIFIED session email — NEVER read from the body — so a
|
|
10
|
+
* developer can only mint a code for themselves.
|
|
11
|
+
*
|
|
12
|
+
* TENANT-SCOPED CODE (roadtoa3 postmortem, 2026-07-12): the mint also carries the
|
|
13
|
+
* developer's OWN tenant (their invite's vendor, the same resolution /dev renders with —
|
|
14
|
+
* first membership, else the session resource; never client input). The MCP stores it
|
|
15
|
+
* with the code so `tot login --code` lands the terminal ALREADY SCOPED to that vendor —
|
|
16
|
+
* `tot start` goes straight to their store's source, no client_list round-trip, no
|
|
17
|
+
* "no stores you can build on yet". See handoff
|
|
18
|
+
* `2026-07-12-tot-mcp-cli-code-tenant-binding` for the MCP half (store tenant with the
|
|
19
|
+
* code + write the durable subject-capability binding at mint).
|
|
20
|
+
*
|
|
21
|
+
* The response `command` CONTAINS the single-use token; it is returned only to this
|
|
22
|
+
* authenticated developer's own browser and is never logged server-side.
|
|
23
|
+
*/
|
|
24
|
+
import type { APIContext } from "astro";
|
|
25
|
+
import { readViewerSession } from "@/lib/auth/route";
|
|
26
|
+
import { mintCliSignInCommand } from "@/lib/dev/cliSignInCode";
|
|
27
|
+
import { mintActivityIngestToken } from "@/lib/dev/activityIngestToken";
|
|
28
|
+
import { readKv } from "@/lib/env";
|
|
29
|
+
|
|
30
|
+
export const prerender = false;
|
|
31
|
+
|
|
32
|
+
function json(body: unknown, status = 200): Response {
|
|
33
|
+
return new Response(JSON.stringify(body), {
|
|
34
|
+
status,
|
|
35
|
+
headers: { "content-type": "application/json" },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
40
|
+
const session = await readViewerSession(context);
|
|
41
|
+
if (!session) return json({ error: "not signed in" }, 401);
|
|
42
|
+
// Same gate the /dev page applies (hasStoreAccess): a real developer capability. A
|
|
43
|
+
// no-capability session has no scoped CLI session to mint.
|
|
44
|
+
if (!session.capability || session.capability === "none") {
|
|
45
|
+
return json({ error: "no developer access on this session", reason: "notEligible" }, 403);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The developer's own vendor — the tenant their invite granted (same resolution /dev
|
|
49
|
+
// renders with). Rides with the code so the CLI session lands on this store directly.
|
|
50
|
+
const tenant = session.tenants?.[0]?.resource ?? session.resource ?? null;
|
|
51
|
+
|
|
52
|
+
// Mint the local→hosted activity-bridge credential alongside the CLI code (best
|
|
53
|
+
// effort — a runtime without the SESSION KV binding just omits it, and the local
|
|
54
|
+
// loop runs with no hosted signal, same as before this existed).
|
|
55
|
+
let activityBridge: { token: string; url: string } | null = null;
|
|
56
|
+
const sessionKv = await readKv("SESSION");
|
|
57
|
+
if (sessionKv && tenant) {
|
|
58
|
+
const { token } = await mintActivityIngestToken(sessionKv, {
|
|
59
|
+
email: session.email,
|
|
60
|
+
tenant,
|
|
61
|
+
capability: session.capability,
|
|
62
|
+
});
|
|
63
|
+
activityBridge = { token, url: context.url.origin };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const result = await mintCliSignInCommand(session.email, tenant, {}, activityBridge);
|
|
67
|
+
if (!result.ok) {
|
|
68
|
+
// 503 = seam not configured / MCP route unmounted; 502 = MCP fault. Surface neither
|
|
69
|
+
// the token nor internal detail beyond a terse reason.
|
|
70
|
+
return json({ error: "CLI sign-in code unavailable", reason: result.reason }, result.status);
|
|
71
|
+
}
|
|
72
|
+
return json({ command: result.command, expiresAt: result.expiresAt });
|
|
73
|
+
}
|