@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,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET /api/admin/orders/:id` — one order's full detail for the Orders tab (U2).
|
|
3
|
+
*
|
|
4
|
+
* Server-side proxy to tot-foxycart `GET /commerce/orders/:orderId` for the
|
|
5
|
+
* resolved tenant + env. Bearer stays server-side; response is the FoxyOrderDetail
|
|
6
|
+
* DTO. `verificationDetail` is empty until U4 fills it — consumers tolerate that.
|
|
7
|
+
* Read-only (scope-locks).
|
|
8
|
+
*/
|
|
9
|
+
import type { APIContext } from "astro";
|
|
10
|
+
import { readEnv } from "@/lib/env";
|
|
11
|
+
import {
|
|
12
|
+
createFoxyOrderClient,
|
|
13
|
+
orderApiBaseUrl,
|
|
14
|
+
OrderApiError,
|
|
15
|
+
} from "@/lib/admin/orderClient";
|
|
16
|
+
|
|
17
|
+
export const prerender = false;
|
|
18
|
+
|
|
19
|
+
const JSON_HEADERS = {
|
|
20
|
+
"content-type": "application/json; charset=utf-8",
|
|
21
|
+
"cache-control": "no-store",
|
|
22
|
+
"x-robots-tag": "noindex, nofollow",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function jsonError(code: string, status: number): Response {
|
|
26
|
+
return new Response(JSON.stringify({ error: code }), {
|
|
27
|
+
status,
|
|
28
|
+
headers: JSON_HEADERS,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
33
|
+
const { tenant, customizationEnv } = context.locals;
|
|
34
|
+
const orderId = context.params.id;
|
|
35
|
+
if (!orderId) return jsonError("order_not_found", 404);
|
|
36
|
+
|
|
37
|
+
const bearer = await readEnv("FOXY_ORDER_API_KEY");
|
|
38
|
+
if (!bearer) return jsonError("not_provisioned", 503);
|
|
39
|
+
|
|
40
|
+
const client = createFoxyOrderClient({
|
|
41
|
+
baseUrl: orderApiBaseUrl(customizationEnv),
|
|
42
|
+
appDomain: tenant.appDomain,
|
|
43
|
+
bearer,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const order = await client.getOrder(orderId);
|
|
48
|
+
return new Response(JSON.stringify({ order }), { headers: JSON_HEADERS });
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (err instanceof OrderApiError) return jsonError(err.code, err.status);
|
|
51
|
+
return jsonError("unexpected", 500);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET /api/admin/orders` — internal admin route backing the Orders tab (U2).
|
|
3
|
+
*
|
|
4
|
+
* Server-side proxy to tot-foxycart's order READ API for the resolved tenant +
|
|
5
|
+
* environment. The per-tenant bearer key stays on the Worker (never in the
|
|
6
|
+
* response); the browser only ever sees order DTO JSON. Read-only (scope-locks).
|
|
7
|
+
* U3 consumes this and maps the DTO → OrdersViewModel.
|
|
8
|
+
*
|
|
9
|
+
* Access control matches the rest of the admin surface (owned by the admin
|
|
10
|
+
* host/approver layer) — same posture as `api/evidence.json.ts`; no new gate here.
|
|
11
|
+
*/
|
|
12
|
+
import type { APIContext } from "astro";
|
|
13
|
+
import type { FoxyOrderListFilters } from "@tot/public-runtime";
|
|
14
|
+
import { readEnv } from "@/lib/env";
|
|
15
|
+
import {
|
|
16
|
+
createFoxyOrderClient,
|
|
17
|
+
orderApiBaseUrl,
|
|
18
|
+
OrderApiError,
|
|
19
|
+
} from "@/lib/admin/orderClient";
|
|
20
|
+
|
|
21
|
+
export const prerender = false;
|
|
22
|
+
|
|
23
|
+
const JSON_HEADERS = {
|
|
24
|
+
"content-type": "application/json; charset=utf-8",
|
|
25
|
+
"cache-control": "no-store",
|
|
26
|
+
"x-robots-tag": "noindex, nofollow",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function jsonError(code: string, status: number): Response {
|
|
30
|
+
return new Response(JSON.stringify({ error: code }), {
|
|
31
|
+
status,
|
|
32
|
+
headers: JSON_HEADERS,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
37
|
+
const { tenant, customizationEnv } = context.locals;
|
|
38
|
+
|
|
39
|
+
const bearer = await readEnv("FOXY_ORDER_API_KEY");
|
|
40
|
+
if (!bearer) {
|
|
41
|
+
// Worker secret not provisioned for this env — a config error, not the
|
|
42
|
+
// caller's fault. Never echo the (missing) key.
|
|
43
|
+
return jsonError("not_provisioned", 503);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const url = new URL(context.request.url);
|
|
47
|
+
const q = url.searchParams;
|
|
48
|
+
const limitRaw = q.get("limit");
|
|
49
|
+
const filters: FoxyOrderListFilters = {
|
|
50
|
+
queue: q.get("queue") ?? undefined,
|
|
51
|
+
status: q.get("status") ?? undefined,
|
|
52
|
+
from: q.get("from") ?? undefined,
|
|
53
|
+
to: q.get("to") ?? undefined,
|
|
54
|
+
limit: limitRaw != null ? Number(limitRaw) : undefined,
|
|
55
|
+
cursor: q.get("cursor") ?? undefined,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const client = createFoxyOrderClient({
|
|
59
|
+
baseUrl: orderApiBaseUrl(customizationEnv),
|
|
60
|
+
appDomain: tenant.appDomain,
|
|
61
|
+
bearer,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const data = await client.listOrders(filters);
|
|
66
|
+
return new Response(JSON.stringify(data), { headers: JSON_HEADERS });
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err instanceof OrderApiError) return jsonError(err.code, err.status);
|
|
69
|
+
return jsonError("unexpected", 500);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/auth/logout — end the signed-in session.
|
|
3
|
+
*
|
|
4
|
+
* Revokes the server-side record (so the session can't be reused even if the
|
|
5
|
+
* cookie is replayed) and clears the cookie, then bounces to the login page.
|
|
6
|
+
* Exempt from the login gate (it's under /api/auth/), so a viewer can always
|
|
7
|
+
* sign out.
|
|
8
|
+
*/
|
|
9
|
+
import type { APIContext } from "astro";
|
|
10
|
+
import { getSessionStore, clearSessionCookie } from "@/lib/auth/route";
|
|
11
|
+
import { revokeSession, SESSION_COOKIE } from "@/lib/auth/session";
|
|
12
|
+
|
|
13
|
+
export const prerender = false;
|
|
14
|
+
|
|
15
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
16
|
+
const sid = context.cookies.get(SESSION_COOKIE)?.value ?? null;
|
|
17
|
+
const store = await getSessionStore();
|
|
18
|
+
if (store) await revokeSession(store, sid);
|
|
19
|
+
clearSessionCookie(context);
|
|
20
|
+
return context.redirect("/auth/login", 303);
|
|
21
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/auth/magic-exchange — exchange a magic-link token for a signed-in session.
|
|
3
|
+
*
|
|
4
|
+
* The POST half of the magic-link flow (see /auth/magic for the GET-safe landing). The
|
|
5
|
+
* one-time `signInToken` (minted by tot20 via developerInvites `issueSignInToken:true`,
|
|
6
|
+
* embedded in the storefront's branded email) is exchanged HERE — only on a POST, only
|
|
7
|
+
* from the human clicking "Confirm sign in", never on a GET (inbox link-scanners fire
|
|
8
|
+
* GET and would burn the token).
|
|
9
|
+
*
|
|
10
|
+
* tot20 verifies + BURNS the token (single-use), resolves memberships fresh, and returns
|
|
11
|
+
* a signed federated `id_token` — identical contract to /api/auth/verify. We verify that
|
|
12
|
+
* assertion OFFLINE against tot's JWKS and derive the session from its claims (never trust
|
|
13
|
+
* unsigned JSON). Fail closed on any gap.
|
|
14
|
+
*/
|
|
15
|
+
import type { APIContext } from "astro";
|
|
16
|
+
import {
|
|
17
|
+
getAccessClient,
|
|
18
|
+
getSessionStore,
|
|
19
|
+
setSessionCookie,
|
|
20
|
+
safeNext,
|
|
21
|
+
} from "@/lib/auth/route";
|
|
22
|
+
import {
|
|
23
|
+
getFederatedVerifier,
|
|
24
|
+
sessionFromFederatedClaims,
|
|
25
|
+
} from "@/lib/auth/identityToken";
|
|
26
|
+
import { createSession, DEFAULT_SESSION_TTL_SECONDS } from "@/lib/auth/session";
|
|
27
|
+
|
|
28
|
+
export const prerender = false;
|
|
29
|
+
|
|
30
|
+
/** Back to the landing page carrying an error (no token in the URL on failure). */
|
|
31
|
+
function magicRedirect(
|
|
32
|
+
context: APIContext,
|
|
33
|
+
params: { error?: string; next?: string },
|
|
34
|
+
): Response {
|
|
35
|
+
const q = new URLSearchParams();
|
|
36
|
+
if (params.error) q.set("error", params.error);
|
|
37
|
+
if (params.next && params.next !== "/dashboard") q.set("next", params.next);
|
|
38
|
+
const qs = q.toString();
|
|
39
|
+
return context.redirect(`/auth/magic${qs ? `?${qs}` : ""}`, 303);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
43
|
+
let token = "";
|
|
44
|
+
let next = "/dashboard";
|
|
45
|
+
try {
|
|
46
|
+
const form = await context.request.formData();
|
|
47
|
+
token = String(form.get("token") ?? "").trim();
|
|
48
|
+
next = safeNext(String(form.get("next") ?? "/dashboard"));
|
|
49
|
+
} catch {
|
|
50
|
+
return magicRedirect(context, { error: "invalid", next });
|
|
51
|
+
}
|
|
52
|
+
if (!token) return magicRedirect(context, { error: "missing", next });
|
|
53
|
+
|
|
54
|
+
const client = await getAccessClient();
|
|
55
|
+
const store = await getSessionStore();
|
|
56
|
+
if (!client || !store) {
|
|
57
|
+
return magicRedirect(context, { error: "unconfigured", next });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Consume the token (single-use; burned server-side by tot20).
|
|
61
|
+
const result = await client.exchangeSignInToken(token);
|
|
62
|
+
if (!result.ok) {
|
|
63
|
+
// reason ∈ invalid|expired|error (tot20's "used" normalizes to invalid).
|
|
64
|
+
const error = result.reason === "expired" ? "expired" : "invalid";
|
|
65
|
+
return magicRedirect(context, { error, next });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Prove authorization offline — fail closed with no flat-JSON fallback.
|
|
69
|
+
const idToken = result.id_token;
|
|
70
|
+
if (!idToken) {
|
|
71
|
+
console.error("auth/magic-exchange: no id_token on exchange; failing closed");
|
|
72
|
+
return magicRedirect(context, { error: "verify_failed", next });
|
|
73
|
+
}
|
|
74
|
+
const verifier = await getFederatedVerifier();
|
|
75
|
+
if (!verifier) {
|
|
76
|
+
console.error("auth/magic-exchange: federated verifier unconfigured");
|
|
77
|
+
return magicRedirect(context, { error: "unconfigured", next });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let derived;
|
|
81
|
+
try {
|
|
82
|
+
const claims = await verifier.verify(idToken); // enforces aud === STOREFRONT_APP_DOMAIN
|
|
83
|
+
derived = sessionFromFederatedClaims(claims);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error("auth/magic-exchange: id_token verification failed; failing closed", {
|
|
86
|
+
error: err instanceof Error ? err.message : String(err),
|
|
87
|
+
});
|
|
88
|
+
return magicRedirect(context, { error: "verify_failed", next });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
92
|
+
const { id } = await createSession(
|
|
93
|
+
store,
|
|
94
|
+
{
|
|
95
|
+
email: derived.email,
|
|
96
|
+
resource: derived.resource,
|
|
97
|
+
roles: [],
|
|
98
|
+
capability: "member",
|
|
99
|
+
tenants: derived.tenants,
|
|
100
|
+
staff: derived.staff,
|
|
101
|
+
},
|
|
102
|
+
DEFAULT_SESSION_TTL_SECONDS,
|
|
103
|
+
nowSeconds,
|
|
104
|
+
);
|
|
105
|
+
setSessionCookie(context, id, DEFAULT_SESSION_TTL_SECONDS);
|
|
106
|
+
return context.redirect(next, 303);
|
|
107
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/auth/send — ask tot20 to email a one-time access code (FEDERATED).
|
|
3
|
+
*
|
|
4
|
+
* Federated multi-tenant sign-in: storefront authenticates as its OWN operator
|
|
5
|
+
* identity and asks tot20 to mail a code to the email if it holds a role on ANY
|
|
6
|
+
* tenant — no `resource` is sent (the resource-scoped `/access-code/send` stays
|
|
7
|
+
* for preview-host gating; console sign-in is federated). ENUMERATION-RESISTANT:
|
|
8
|
+
* the response is identical whether or not the email is eligible — we always
|
|
9
|
+
* redirect to the "enter your code" step. Accepts an HTML form post (no JS needed).
|
|
10
|
+
*/
|
|
11
|
+
import type { APIContext } from "astro";
|
|
12
|
+
import { readKv } from "@/lib/env";
|
|
13
|
+
import {
|
|
14
|
+
EMAIL_RE,
|
|
15
|
+
getAccessClient,
|
|
16
|
+
safeNext,
|
|
17
|
+
loginRedirect,
|
|
18
|
+
} from "@/lib/auth/route";
|
|
19
|
+
import {
|
|
20
|
+
devAuthEnabled,
|
|
21
|
+
devAuthCode,
|
|
22
|
+
storeDevCode,
|
|
23
|
+
DEV_AUTH_CODE_TTL_SECONDS,
|
|
24
|
+
} from "@/lib/auth/devAuth";
|
|
25
|
+
|
|
26
|
+
export const prerender = false;
|
|
27
|
+
|
|
28
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
29
|
+
let email = "";
|
|
30
|
+
let next = "/";
|
|
31
|
+
try {
|
|
32
|
+
const form = await context.request.formData();
|
|
33
|
+
email = String(form.get("email") ?? "").trim().toLowerCase();
|
|
34
|
+
next = safeNext(String(form.get("next") ?? "/"));
|
|
35
|
+
} catch {
|
|
36
|
+
return loginRedirect(context, { error: "invalid", next });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Bad address → send them back to step 1 with a gentle error (no code step).
|
|
40
|
+
if (!EMAIL_RE.test(email)) {
|
|
41
|
+
return loginRedirect(context, { error: "bad_email", next });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// LOCAL DEV AUTH (send side). `import.meta.env.DEV` is a STATIC boolean — this
|
|
45
|
+
// whole branch is tree-shaken out of the production build (same mechanism as the
|
|
46
|
+
// dev guards in middleware/index.ts). It NEVER weakens the hosted path.
|
|
47
|
+
//
|
|
48
|
+
// On localhost tot20 isn't reachable, so no email can be sent. When a developer
|
|
49
|
+
// opts in with `DEV_AUTH=1` in `.dev.vars`, we instead mint a one-time code,
|
|
50
|
+
// store it in the SESSION KV, and PRINT it to the server console so the developer
|
|
51
|
+
// can complete sign-in locally. (The verify side that ACCEPTS this code + mints a
|
|
52
|
+
// session without tot20 is the security-sensitive half — see the handoff
|
|
53
|
+
// `2026-07-09-storefront-local-dev-auth-mode.md`; until it lands, the code is
|
|
54
|
+
// stored + printed but sign-in completes only once that verify path is wired.)
|
|
55
|
+
if (import.meta.env.DEV && (await devAuthEnabled())) {
|
|
56
|
+
const kv = await readKv("SESSION");
|
|
57
|
+
if (kv) {
|
|
58
|
+
const code = devAuthCode();
|
|
59
|
+
await storeDevCode(kv, email, code);
|
|
60
|
+
// eslint-disable-next-line no-console -- dev-only server-console code delivery
|
|
61
|
+
console.log(
|
|
62
|
+
`\n 🔑 [DEV AUTH] sign-in code for ${email}: ${code}\n` +
|
|
63
|
+
` valid ${DEV_AUTH_CODE_TTL_SECONDS / 60}m · printed here only (no email in local dev)\n`,
|
|
64
|
+
);
|
|
65
|
+
} else {
|
|
66
|
+
// eslint-disable-next-line no-console -- dev-only diagnostic
|
|
67
|
+
console.warn(
|
|
68
|
+
"[DEV AUTH] DEV_AUTH=1 but no SESSION KV is bound — cannot store a dev code.",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
// Same opaque redirect as the real path (keeps the UX identical).
|
|
72
|
+
return loginRedirect(context, { sent: true, email, next });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const client = await getAccessClient();
|
|
76
|
+
if (!client) {
|
|
77
|
+
// Integration not configured — don't pretend we sent a code.
|
|
78
|
+
return loginRedirect(context, { error: "unconfigured", email, next });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Fire-and-neutral: even a failure here resolves opaque so we never leak
|
|
82
|
+
// whether the address is a role-holder.
|
|
83
|
+
await client.sendFederatedCode(email);
|
|
84
|
+
return loginRedirect(context, { sent: true, email, next });
|
|
85
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/auth/verify — verify the one-time code and mint the FEDERATED,
|
|
3
|
+
* multi-tenant signed-in session.
|
|
4
|
+
*
|
|
5
|
+
* Federated sign-in: storefront calls tot20's `/access-code/federated-verify`
|
|
6
|
+
* (as its own operator identity, no `resource`). tot20 checks the code and returns
|
|
7
|
+
* a signed `id_token` whose `aud` = the STOREFRONT app identity (not a tenant) and
|
|
8
|
+
* whose `tenants[]` carries ALL of the email's memberships.
|
|
9
|
+
*
|
|
10
|
+
* We verify that assertion OFFLINE against tot's JWKS with `aud` pinned to the
|
|
11
|
+
* storefront's configured app identity (`STOREFRONT_APP_DOMAIN`), then bind EVERY
|
|
12
|
+
* membership into the session — so the dashboard picker sees every vendor. Fail
|
|
13
|
+
* closed if the signed assertion is missing or verification fails (never trust the
|
|
14
|
+
* flat JSON). See `@/lib/auth/identityToken`.
|
|
15
|
+
*/
|
|
16
|
+
import type { APIContext } from "astro";
|
|
17
|
+
import { readKv } from "@/lib/env";
|
|
18
|
+
import {
|
|
19
|
+
getAccessClient,
|
|
20
|
+
getSessionStore,
|
|
21
|
+
setSessionCookie,
|
|
22
|
+
safeNext,
|
|
23
|
+
loginRedirect,
|
|
24
|
+
} from "@/lib/auth/route";
|
|
25
|
+
import {
|
|
26
|
+
getFederatedVerifier,
|
|
27
|
+
sessionFromFederatedClaims,
|
|
28
|
+
} from "@/lib/auth/identityToken";
|
|
29
|
+
import { createSession, DEFAULT_SESSION_TTL_SECONDS } from "@/lib/auth/session";
|
|
30
|
+
import { devAuthEnabled, consumeDevCode, devSessionFields } from "@/lib/auth/devAuth";
|
|
31
|
+
|
|
32
|
+
export const prerender = false;
|
|
33
|
+
|
|
34
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
35
|
+
let email = "";
|
|
36
|
+
let code = "";
|
|
37
|
+
let next = "/";
|
|
38
|
+
try {
|
|
39
|
+
const form = await context.request.formData();
|
|
40
|
+
email = String(form.get("email") ?? "").trim().toLowerCase();
|
|
41
|
+
code = String(form.get("code") ?? "").trim();
|
|
42
|
+
next = safeNext(String(form.get("next") ?? "/"));
|
|
43
|
+
} catch {
|
|
44
|
+
return loginRedirect(context, { error: "invalid", next });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!email || !code) {
|
|
48
|
+
return loginRedirect(context, { sent: true, email, error: "invalid", next });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// LOCAL DEV AUTH (verify side). `import.meta.env.DEV` is a STATIC boolean — this
|
|
52
|
+
// whole branch (and the devAuth imports) are tree-shaken out of the production
|
|
53
|
+
// build, so the hosted path below (offline JWKS verification, fail-closed) is
|
|
54
|
+
// never weakened. Second gate: the DEV_AUTH opt-in. Consume the console-printed
|
|
55
|
+
// code stored by /api/auth/send and mint a local session WITHOUT tot20 — the
|
|
56
|
+
// localhost completion of the invite→sign-in loop. See @/lib/auth/devAuth.
|
|
57
|
+
if (import.meta.env.DEV && (await devAuthEnabled())) {
|
|
58
|
+
const store = await getSessionStore();
|
|
59
|
+
const kv = await readKv("SESSION");
|
|
60
|
+
if (!store || !kv) {
|
|
61
|
+
return loginRedirect(context, { sent: true, email, error: "unconfigured", next });
|
|
62
|
+
}
|
|
63
|
+
if (!(await consumeDevCode(kv, email, code))) {
|
|
64
|
+
// Same "code didn't match" UX as the real path (wrong/expired/replayed code).
|
|
65
|
+
return loginRedirect(context, { sent: true, email, error: "invalid", next });
|
|
66
|
+
}
|
|
67
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
68
|
+
const { id } = await createSession(
|
|
69
|
+
store,
|
|
70
|
+
await devSessionFields(email),
|
|
71
|
+
DEFAULT_SESSION_TTL_SECONDS,
|
|
72
|
+
nowSeconds,
|
|
73
|
+
);
|
|
74
|
+
setSessionCookie(context, id, DEFAULT_SESSION_TTL_SECONDS);
|
|
75
|
+
return context.redirect(next, 303);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const client = await getAccessClient();
|
|
79
|
+
const store = await getSessionStore();
|
|
80
|
+
if (!client || !store) {
|
|
81
|
+
return loginRedirect(context, { sent: true, email, error: "unconfigured", next });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result = await client.verifyFederatedCode(email, code);
|
|
85
|
+
if (!result.ok) {
|
|
86
|
+
// Show the code step again with the specific reason (invalid/expired/too_many_attempts).
|
|
87
|
+
return loginRedirect(context, { sent: true, email, error: result.reason, next });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Prove authorization offline. Fail closed (no flat-JSON fallback) if the signed
|
|
91
|
+
// assertion is missing or fails verification.
|
|
92
|
+
const idToken = result.id_token;
|
|
93
|
+
if (!idToken) {
|
|
94
|
+
console.error("auth/verify: no id_token on federated result; failing closed");
|
|
95
|
+
return loginRedirect(context, { sent: true, email, error: "verify_failed", next });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// aud is the STOREFRONT app identity (STOREFRONT_APP_DOMAIN), not a tenant.
|
|
99
|
+
const verifier = await getFederatedVerifier();
|
|
100
|
+
if (!verifier) {
|
|
101
|
+
console.error("auth/verify: federated verifier unconfigured (TOT_CORE_URL/STOREFRONT_APP_DOMAIN)");
|
|
102
|
+
return loginRedirect(context, { sent: true, email, error: "unconfigured", next });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let derived;
|
|
106
|
+
try {
|
|
107
|
+
const claims = await verifier.verify(idToken); // enforces aud === STOREFRONT_APP_DOMAIN
|
|
108
|
+
// Bind EVERY membership — no single-tenant scoping (multi-vendor picker input).
|
|
109
|
+
derived = sessionFromFederatedClaims(claims);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error("auth/verify: federated id_token verification failed; failing closed", {
|
|
112
|
+
error: err instanceof Error ? err.message : String(err),
|
|
113
|
+
});
|
|
114
|
+
return loginRedirect(context, { sent: true, email, error: "verify_failed", next });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
118
|
+
const { id } = await createSession(
|
|
119
|
+
store,
|
|
120
|
+
{
|
|
121
|
+
email: derived.email,
|
|
122
|
+
// The verified aud (storefront app identity) — the console session's scope.
|
|
123
|
+
resource: derived.resource,
|
|
124
|
+
// Per-tenant roles live in tenants[]; the console session itself is role-neutral.
|
|
125
|
+
roles: [],
|
|
126
|
+
capability: "member",
|
|
127
|
+
tenants: derived.tenants,
|
|
128
|
+
staff: derived.staff,
|
|
129
|
+
},
|
|
130
|
+
DEFAULT_SESSION_TTL_SECONDS,
|
|
131
|
+
nowSeconds,
|
|
132
|
+
);
|
|
133
|
+
setSessionCookie(context, id, DEFAULT_SESSION_TTL_SECONDS);
|
|
134
|
+
return context.redirect(next, 303);
|
|
135
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/cache-purge — bump a tenant's edge page-cache version.
|
|
3
|
+
*
|
|
4
|
+
* The ETL load step calls this per tenant after a (re)migration so freshly
|
|
5
|
+
* migrated content is visible immediately, rather than waiting out the page
|
|
6
|
+
* TTL. This closes the loop that HttpToTClient.purgeTenant() (catalog API cache)
|
|
7
|
+
* only half-covered: that purges the data cache; this purges the rendered HTML.
|
|
8
|
+
*
|
|
9
|
+
* Auth: `Authorization: Bearer <CACHE_PURGE_SECRET>`. JSON bodies are exempt from
|
|
10
|
+
* Astro's CSRF origin check, so the server-to-server ETL caller works.
|
|
11
|
+
*/
|
|
12
|
+
import type { APIContext } from "astro";
|
|
13
|
+
import { readEnv, readKv } from "@/lib/env";
|
|
14
|
+
import { bumpCacheVersion } from "@/lib/edgeCache";
|
|
15
|
+
|
|
16
|
+
export const prerender = false;
|
|
17
|
+
|
|
18
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
19
|
+
const secret = await readEnv("CACHE_PURGE_SECRET");
|
|
20
|
+
const auth = context.request.headers.get("authorization");
|
|
21
|
+
if (!secret || auth !== `Bearer ${secret}`) {
|
|
22
|
+
return new Response("Unauthorized", { status: 401 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const kv = await readKv("TENANT_CACHE");
|
|
26
|
+
if (!kv) {
|
|
27
|
+
return new Response("No cache store bound", { status: 503 });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let tenantId: string | undefined;
|
|
31
|
+
try {
|
|
32
|
+
tenantId = ((await context.request.json()) as { tenantId?: string })?.tenantId;
|
|
33
|
+
} catch {
|
|
34
|
+
/* fall through to 400 */
|
|
35
|
+
}
|
|
36
|
+
if (!tenantId) {
|
|
37
|
+
return new Response("tenantId required", { status: 400 });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const version = await bumpCacheVersion(kv, tenantId);
|
|
41
|
+
return new Response(JSON.stringify({ tenantId, version }), {
|
|
42
|
+
headers: { "content-type": "application/json" },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET /api/checkout-style.css` — the stable, per-tenant storefront URL that hosts
|
|
3
|
+
* the published checkout styling as `text/css`. This URL is what U11's publisher
|
|
4
|
+
* points FoxyCart's checkout-template `content_url` at (PUT /theme/foxy) and what
|
|
5
|
+
* Foxy fetches server-side when it (re)caches the checkout template.
|
|
6
|
+
*
|
|
7
|
+
* The tenant + environment are self-identifying from the request Host (Foxy's
|
|
8
|
+
* fetch carries no session): the middleware resolves `locals.tenant` +
|
|
9
|
+
* `locals.customizationEnv`, and we serve exactly that tenant's published style
|
|
10
|
+
* for that env (live on prod hosts, test behind a preview token). Falls back to
|
|
11
|
+
* the neutral DEFAULT_CHECKOUT_STYLE tokens when nothing is published, so the URL
|
|
12
|
+
* is always a valid, non-empty stylesheet.
|
|
13
|
+
*
|
|
14
|
+
* ASCII-safety (Foxy cache is not Unicode-safe) is enforced inside
|
|
15
|
+
* buildCheckoutStyleCss. No secrets, no cross-service call — read-only from KV.
|
|
16
|
+
*/
|
|
17
|
+
import type { APIContext } from "astro";
|
|
18
|
+
import { buildCheckoutStyleCss } from "@tot/public-runtime";
|
|
19
|
+
import { readKv } from "@/lib/env";
|
|
20
|
+
|
|
21
|
+
export const prerender = false;
|
|
22
|
+
|
|
23
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
24
|
+
const { tenant, customizationEnv } = context.locals;
|
|
25
|
+
const kv = await readKv("TENANT_CACHE");
|
|
26
|
+
const kvGet = kv ? (key: string) => kv.get(key) : async () => null;
|
|
27
|
+
|
|
28
|
+
const css = await buildCheckoutStyleCss(
|
|
29
|
+
kvGet,
|
|
30
|
+
tenant.tenant_id,
|
|
31
|
+
customizationEnv,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return new Response(css, {
|
|
35
|
+
headers: {
|
|
36
|
+
"content-type": "text/css; charset=utf-8",
|
|
37
|
+
// Foxy caches on its side via /theme/foxy/refresh; keep the origin fresh so
|
|
38
|
+
// a re-cache always pulls the latest published styling.
|
|
39
|
+
"cache-control": "no-store",
|
|
40
|
+
"x-robots-tag": "noindex, nofollow",
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/cli/policy — the storefront-hosted, KV-dynamic version-support policy
|
|
3
|
+
* for the `tot` CLI (update-awareness Layer 2). PUBLIC, unauthenticated: it only
|
|
4
|
+
* answers "which CLI versions does ToT still support / recommend?", which is not
|
|
5
|
+
* secret and must be readable by an unauthenticated CLI on first run.
|
|
6
|
+
*
|
|
7
|
+
* The CLI's detached update-check refresh fetches this best-effort and hands the
|
|
8
|
+
* body to recordServerPolicy() (packages/cli/src/update-check.mjs), so the next
|
|
9
|
+
* `tot` run renders ToT's support verdict. Because the value lives in KV, ToT can
|
|
10
|
+
* raise the floor / recommend a new rc WITHOUT a storefront redeploy.
|
|
11
|
+
*
|
|
12
|
+
* Contract: JSON `{ minSupported?, recommended?, message? }`. When the KV key is
|
|
13
|
+
* unset (or no KV in this runtime), returns `{}` — a safe no-op, so shipping this
|
|
14
|
+
* route changes nothing for any CLI until ToT actually sets the value.
|
|
15
|
+
*
|
|
16
|
+
* SETTING THE POLICY (no code change, no redeploy — the whole point):
|
|
17
|
+
* wrangler kv key put --binding=TENANT_CACHE --env preview cli:policy \
|
|
18
|
+
* '{"minSupported":"1.3.1","recommended":"1.3.1-rc.6","message":"rc.5 has a bad build; move to rc.6"}'
|
|
19
|
+
* Clear it (back to no-op) with: wrangler kv key delete --binding=TENANT_CACHE --env preview cli:policy
|
|
20
|
+
* A small authed PUT is a possible follow-up for nicer ergonomics; the KV put is
|
|
21
|
+
* the idiomatic write path today. See docs/architecture/dev-experience-hosted-vs-local.md.
|
|
22
|
+
*/
|
|
23
|
+
import type { APIContext } from "astro";
|
|
24
|
+
import { readKv } from "@/lib/env";
|
|
25
|
+
|
|
26
|
+
export const prerender = false;
|
|
27
|
+
|
|
28
|
+
/** KV key holding the (global, not per-tenant) CLI version-support policy. */
|
|
29
|
+
export const CLI_POLICY_KV_KEY = "cli:policy";
|
|
30
|
+
|
|
31
|
+
interface CliPolicy {
|
|
32
|
+
minSupported?: string;
|
|
33
|
+
recommended?: string;
|
|
34
|
+
message?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function json(body: unknown, status = 200): Response {
|
|
38
|
+
return new Response(JSON.stringify(body), {
|
|
39
|
+
status,
|
|
40
|
+
headers: {
|
|
41
|
+
"content-type": "application/json",
|
|
42
|
+
// Public + safe to cache briefly at the edge; the CLI itself only refreshes
|
|
43
|
+
// once/day, so a short TTL keeps ToT's edits propagating quickly.
|
|
44
|
+
"cache-control": "public, max-age=300",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Keep only the three known string fields — never echo arbitrary stored data. */
|
|
50
|
+
function sanitize(raw: unknown): CliPolicy {
|
|
51
|
+
if (!raw || typeof raw !== "object") return {};
|
|
52
|
+
const r = raw as Record<string, unknown>;
|
|
53
|
+
const out: CliPolicy = {};
|
|
54
|
+
if (typeof r.minSupported === "string") out.minSupported = r.minSupported;
|
|
55
|
+
if (typeof r.recommended === "string") out.recommended = r.recommended;
|
|
56
|
+
if (typeof r.message === "string") out.message = r.message;
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function GET(_context: APIContext): Promise<Response> {
|
|
61
|
+
const kv = await readKv("TENANT_CACHE");
|
|
62
|
+
if (!kv) return json({}); // no KV in this runtime (dev) — safe no-op default
|
|
63
|
+
const raw = await kv.get(CLI_POLICY_KV_KEY);
|
|
64
|
+
if (!raw) return json({}); // unset — no-op until ToT sets it
|
|
65
|
+
try {
|
|
66
|
+
return json(sanitize(JSON.parse(raw)));
|
|
67
|
+
} catch {
|
|
68
|
+
return json({}); // malformed stored value — degrade to no-op, never 500
|
|
69
|
+
}
|
|
70
|
+
}
|