@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.
Files changed (310) hide show
  1. package/LICENSE +58 -0
  2. package/README.md +42 -0
  3. package/apps/storefront/astro.config.mjs +66 -0
  4. package/apps/storefront/dev-plugins/dev-loop-state.mjs +147 -0
  5. package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +238 -0
  6. package/apps/storefront/env.d.ts +162 -0
  7. package/apps/storefront/migrations/0000_baseline.sql +54 -0
  8. package/apps/storefront/migrations/0001_products_fts.sql +9 -0
  9. package/apps/storefront/migrations/README.md +54 -0
  10. package/apps/storefront/migrations/meta/0000_snapshot.json +347 -0
  11. package/apps/storefront/migrations/meta/0001_snapshot.json +347 -0
  12. package/apps/storefront/migrations/meta/_journal.json +20 -0
  13. package/apps/storefront/package.json +42 -0
  14. package/apps/storefront/public/brand/northwind-logo-dark.svg +4 -0
  15. package/apps/storefront/public/brand/northwind-logo-light.svg +4 -0
  16. package/apps/storefront/public/brand/northwind-og.svg +13 -0
  17. package/apps/storefront/public/favicon.svg +5 -0
  18. package/apps/storefront/public/js/dashboard-team.js +147 -0
  19. package/apps/storefront/public/js/dashboard-vendor-search.js +115 -0
  20. package/apps/storefront/public/shared/commerce-chrome.css +369 -0
  21. package/apps/storefront/src/components/Badge.astro +34 -0
  22. package/apps/storefront/src/components/Breadcrumbs.astro +45 -0
  23. package/apps/storefront/src/components/CollectionCard.astro +56 -0
  24. package/apps/storefront/src/components/EmptyState.astro +55 -0
  25. package/apps/storefront/src/components/Img.astro +68 -0
  26. package/apps/storefront/src/components/ProductCard.astro +228 -0
  27. package/apps/storefront/src/components/Section.astro +39 -0
  28. package/apps/storefront/src/components/Seo.astro +70 -0
  29. package/apps/storefront/src/components/WidgetFrame.astro +34 -0
  30. package/apps/storefront/src/components/admin/AdminAIWorkflowsTab.astro +531 -0
  31. package/apps/storefront/src/components/admin/AdminCustomersTab.astro +597 -0
  32. package/apps/storefront/src/components/admin/AdminFoxyCartSettingsPanel.astro +227 -0
  33. package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +465 -0
  34. package/apps/storefront/src/components/admin/AdminProductsTab.astro +901 -0
  35. package/apps/storefront/src/components/admin/AdminReportingTab.astro +435 -0
  36. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +782 -0
  37. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +1223 -0
  38. package/apps/storefront/src/components/auth/AuthBadge.astro +44 -0
  39. package/apps/storefront/src/components/chrome/AnnouncementBar.astro +108 -0
  40. package/apps/storefront/src/components/chrome/MegaMenu.astro +26 -0
  41. package/apps/storefront/src/components/chrome/NavDropdown.astro +201 -0
  42. package/apps/storefront/src/components/chrome/SiteFooter.astro +201 -0
  43. package/apps/storefront/src/components/chrome/SiteHeader.astro +234 -0
  44. package/apps/storefront/src/components/chrome/UtilityNav.astro +79 -0
  45. package/apps/storefront/src/components/commerce/ArticleCard.astro +242 -0
  46. package/apps/storefront/src/components/commerce/BrandGrid.astro +175 -0
  47. package/apps/storefront/src/components/commerce/FoxyLoader.astro +23 -0
  48. package/apps/storefront/src/components/commerce/FoxySidecartStyles.astro +61 -0
  49. package/apps/storefront/src/components/commerce/PriceDisplay.astro +91 -0
  50. package/apps/storefront/src/components/commerce/ProductCarousel.astro +197 -0
  51. package/apps/storefront/src/components/commerce/ProductGrid.astro +75 -0
  52. package/apps/storefront/src/components/commerce/RatingStars.astro +58 -0
  53. package/apps/storefront/src/components/commerce/ReviewList.astro +54 -0
  54. package/apps/storefront/src/components/commerce/ReviewSummary.astro +44 -0
  55. package/apps/storefront/src/components/commerce/TagFilterCloud.astro +149 -0
  56. package/apps/storefront/src/components/compliance/AdultSignatureNotice.astro +37 -0
  57. package/apps/storefront/src/components/compliance/ComplianceNotice.astro +99 -0
  58. package/apps/storefront/src/components/compliance/ExciseTaxNotice.astro +45 -0
  59. package/apps/storefront/src/components/compliance/NicotineWarning.astro +33 -0
  60. package/apps/storefront/src/components/compliance/PactActNotice.astro +35 -0
  61. package/apps/storefront/src/components/compliance/Prop65Warning.astro +80 -0
  62. package/apps/storefront/src/components/compliance/PurchaseLimitNotice.astro +38 -0
  63. package/apps/storefront/src/components/compliance/ShippingRestrictionNotice.astro +35 -0
  64. package/apps/storefront/src/components/compliance/StateEligibilityNotice.astro +53 -0
  65. package/apps/storefront/src/components/home/Hero.astro +164 -0
  66. package/apps/storefront/src/components/islands/AgeGate.tsx +109 -0
  67. package/apps/storefront/src/components/islands/ImageGallery.tsx +237 -0
  68. package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +73 -0
  69. package/apps/storefront/src/components/islands/MobileNav.tsx +239 -0
  70. package/apps/storefront/src/components/islands/QuickView.tsx +138 -0
  71. package/apps/storefront/src/components/islands/RecentlyViewed.tsx +122 -0
  72. package/apps/storefront/src/components/islands/SavedList.tsx +99 -0
  73. package/apps/storefront/src/components/islands/SearchTypeahead.tsx +248 -0
  74. package/apps/storefront/src/components/islands/VariantSelector.tsx +375 -0
  75. package/apps/storefront/src/components/marketing/CardGrid.astro +72 -0
  76. package/apps/storefront/src/components/marketing/CodeSample.astro +20 -0
  77. package/apps/storefront/src/components/marketing/CtaBand.astro +24 -0
  78. package/apps/storefront/src/components/marketing/Faq.astro +37 -0
  79. package/apps/storefront/src/components/marketing/Integrations.astro +63 -0
  80. package/apps/storefront/src/components/marketing/MarketingBlocks.astro +53 -0
  81. package/apps/storefront/src/components/marketing/MarketingCtas.astro +48 -0
  82. package/apps/storefront/src/components/marketing/MarketingHero.astro +72 -0
  83. package/apps/storefront/src/components/marketing/ProofStrip.astro +37 -0
  84. package/apps/storefront/src/components/marketing/SplitCompare.astro +53 -0
  85. package/apps/storefront/src/components/marketing/Steps.astro +36 -0
  86. package/apps/storefront/src/components/marketing/Testimonials.astro +38 -0
  87. package/apps/storefront/src/components/marketing/TrustBar.astro +22 -0
  88. package/apps/storefront/src/components/membership/IllustrativeLabel.astro +56 -0
  89. package/apps/storefront/src/components/membership/TierCards.astro +164 -0
  90. package/apps/storefront/src/components/membership/TierComparisonTable.astro +148 -0
  91. package/apps/storefront/src/components/plp/ActiveFilters.astro +79 -0
  92. package/apps/storefront/src/components/plp/FacetSidebar.astro +199 -0
  93. package/apps/storefront/src/components/plp/Pagination.astro +99 -0
  94. package/apps/storefront/src/components/plp/SortSelect.astro +64 -0
  95. package/apps/storefront/src/components/style-guide/StyleGuideNav.astro +95 -0
  96. package/apps/storefront/src/components/subscription/HowItWorksSteps.astro +67 -0
  97. package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +67 -0
  98. package/apps/storefront/src/components/subscription/SavingsExplainer.astro +101 -0
  99. package/apps/storefront/src/config/adminTargetTenant.ts +28 -0
  100. package/apps/storefront/src/config/devTenantSeed.ts +78 -0
  101. package/apps/storefront/src/config/devTenants.ts +16 -0
  102. package/apps/storefront/src/config/kvTenantResolver.ts +87 -0
  103. package/apps/storefront/src/config/resolver.ts +102 -0
  104. package/apps/storefront/src/config/tenants.ts +87 -0
  105. package/apps/storefront/src/layouts/Layout.astro +368 -0
  106. package/apps/storefront/src/lib/admin/README.md +122 -0
  107. package/apps/storefront/src/lib/admin/ai/approvalMode.ts +21 -0
  108. package/apps/storefront/src/lib/admin/ai/askEnvelope.ts +48 -0
  109. package/apps/storefront/src/lib/admin/ai/audit.ts +14 -0
  110. package/apps/storefront/src/lib/admin/ai/index.ts +6 -0
  111. package/apps/storefront/src/lib/admin/ai/operationalRuns.ts +155 -0
  112. package/apps/storefront/src/lib/admin/ai/recommendation.ts +20 -0
  113. package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +172 -0
  114. package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +65 -0
  115. package/apps/storefront/src/lib/admin/envelope.ts +10 -0
  116. package/apps/storefront/src/lib/admin/evidence/evidenceReference.ts +32 -0
  117. package/apps/storefront/src/lib/admin/memoizeByEnvelope.ts +27 -0
  118. package/apps/storefront/src/lib/admin/orderClient.ts +146 -0
  119. package/apps/storefront/src/lib/admin/seed.ts +54 -0
  120. package/apps/storefront/src/lib/admin/services/mock/aiWorkflows.ts +59 -0
  121. package/apps/storefront/src/lib/admin/services/mock/customers.ts +86 -0
  122. package/apps/storefront/src/lib/admin/services/mock/evidence.ts +99 -0
  123. package/apps/storefront/src/lib/admin/services/mock/foxyCartSettings.ts +78 -0
  124. package/apps/storefront/src/lib/admin/services/mock/fulfillment.ts +62 -0
  125. package/apps/storefront/src/lib/admin/services/mock/index.ts +12 -0
  126. package/apps/storefront/src/lib/admin/services/mock/orders.ts +184 -0
  127. package/apps/storefront/src/lib/admin/services/mock/products.ts +105 -0
  128. package/apps/storefront/src/lib/admin/services/mock/reporting.ts +51 -0
  129. package/apps/storefront/src/lib/admin/services/mock/settings.ts +70 -0
  130. package/apps/storefront/src/lib/admin/services/mock/tenants.ts +29 -0
  131. package/apps/storefront/src/lib/admin/services/orders.ts +300 -0
  132. package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +67 -0
  133. package/apps/storefront/src/lib/admin/viewmodels/customers.ts +56 -0
  134. package/apps/storefront/src/lib/admin/viewmodels/fulfillment.ts +49 -0
  135. package/apps/storefront/src/lib/admin/viewmodels/orders.ts +92 -0
  136. package/apps/storefront/src/lib/admin/viewmodels/products.ts +74 -0
  137. package/apps/storefront/src/lib/admin/viewmodels/reporting.ts +34 -0
  138. package/apps/storefront/src/lib/admin/viewmodels/settings.ts +64 -0
  139. package/apps/storefront/src/lib/adminTenantEnvelope.ts +90 -0
  140. package/apps/storefront/src/lib/analytics/index.ts +47 -0
  141. package/apps/storefront/src/lib/auth/brokerAssertion.ts +136 -0
  142. package/apps/storefront/src/lib/auth/devAuth.ts +90 -0
  143. package/apps/storefront/src/lib/auth/gatePage.ts +81 -0
  144. package/apps/storefront/src/lib/auth/identityToken.ts +341 -0
  145. package/apps/storefront/src/lib/auth/loginGate.ts +119 -0
  146. package/apps/storefront/src/lib/auth/route.ts +146 -0
  147. package/apps/storefront/src/lib/auth/session.ts +203 -0
  148. package/apps/storefront/src/lib/auth/totAccessClient.ts +503 -0
  149. package/apps/storefront/src/lib/basePath.ts +41 -0
  150. package/apps/storefront/src/lib/cfImage.ts +55 -0
  151. package/apps/storefront/src/lib/chrome/model.ts +244 -0
  152. package/apps/storefront/src/lib/chrome/parseConfig.ts +34 -0
  153. package/apps/storefront/src/lib/colors.ts +97 -0
  154. package/apps/storefront/src/lib/commerce/foxyCartSettings.ts +253 -0
  155. package/apps/storefront/src/lib/content-edit/applyDiff.ts +91 -0
  156. package/apps/storefront/src/lib/content-edit/client.ts +230 -0
  157. package/apps/storefront/src/lib/content-edit/index.ts +14 -0
  158. package/apps/storefront/src/lib/content-edit/route.ts +25 -0
  159. package/apps/storefront/src/lib/content-edit/types.ts +19 -0
  160. package/apps/storefront/src/lib/cspStaticHashes.ts +24 -0
  161. package/apps/storefront/src/lib/d1/catalog.ts +366 -0
  162. package/apps/storefront/src/lib/dashboard/staffAdmission.ts +43 -0
  163. package/apps/storefront/src/lib/dashboard/staffVendorAccess.ts +449 -0
  164. package/apps/storefront/src/lib/dashboard/tenantSelection.ts +136 -0
  165. package/apps/storefront/src/lib/dashboard/vendorQuery.ts +61 -0
  166. package/apps/storefront/src/lib/demoGate.ts +113 -0
  167. package/apps/storefront/src/lib/dev/activityIngestToken.ts +78 -0
  168. package/apps/storefront/src/lib/dev/activityStore.ts +72 -0
  169. package/apps/storefront/src/lib/dev/ai-widget-client.js +308 -0
  170. package/apps/storefront/src/lib/dev/apiBase.ts +14 -0
  171. package/apps/storefront/src/lib/dev/cliSignInCode.ts +155 -0
  172. package/apps/storefront/src/lib/dev/devDraft.ts +133 -0
  173. package/apps/storefront/src/lib/dev/hostedCockpit.ts +169 -0
  174. package/apps/storefront/src/lib/dev/livePreview.ts +77 -0
  175. package/apps/storefront/src/lib/dev/runtimeStore.ts +120 -0
  176. package/apps/storefront/src/lib/dev/storeStatus.ts +142 -0
  177. package/apps/storefront/src/lib/diag/redact.ts +20 -0
  178. package/apps/storefront/src/lib/edgeCache.ts +128 -0
  179. package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +62 -0
  180. package/apps/storefront/src/lib/env.ts +60 -0
  181. package/apps/storefront/src/lib/fixtures/sampleProducts.ts +130 -0
  182. package/apps/storefront/src/lib/format.ts +38 -0
  183. package/apps/storefront/src/lib/foxyCommerce.ts +49 -0
  184. package/apps/storefront/src/lib/imageAttrs.ts +55 -0
  185. package/apps/storefront/src/lib/jsonld.ts +166 -0
  186. package/apps/storefront/src/lib/maintenance.ts +71 -0
  187. package/apps/storefront/src/lib/membership/model.ts +97 -0
  188. package/apps/storefront/src/lib/memoryKv.ts +32 -0
  189. package/apps/storefront/src/lib/messaging/messagesClient.ts +143 -0
  190. package/apps/storefront/src/lib/placeholder.ts +99 -0
  191. package/apps/storefront/src/lib/previewReload.ts +125 -0
  192. package/apps/storefront/src/lib/priceBuckets.ts +33 -0
  193. package/apps/storefront/src/lib/rawChrome.ts +505 -0
  194. package/apps/storefront/src/lib/rawMarketingHtml.ts +136 -0
  195. package/apps/storefront/src/lib/search/index.ts +74 -0
  196. package/apps/storefront/src/lib/sections.ts +129 -0
  197. package/apps/storefront/src/lib/securityHeaders.ts +31 -0
  198. package/apps/storefront/src/lib/storyblok/content-model.ts +289 -0
  199. package/apps/storefront/src/lib/storyblok/provider.ts +607 -0
  200. package/apps/storefront/src/lib/subscription/model.ts +68 -0
  201. package/apps/storefront/src/lib/tenantRouting.ts +91 -0
  202. package/apps/storefront/src/lib/tot/ToTClient.ts +177 -0
  203. package/apps/storefront/src/lib/tot/d1Client.ts +62 -0
  204. package/apps/storefront/src/lib/tot/query.ts +249 -0
  205. package/apps/storefront/src/lib/tot/totClientInterface.ts +30 -0
  206. package/apps/storefront/src/lib/url.ts +142 -0
  207. package/apps/storefront/src/lib/wishlist.ts +63 -0
  208. package/apps/storefront/src/middleware/index.ts +455 -0
  209. package/apps/storefront/src/pages/404.astro +61 -0
  210. package/apps/storefront/src/pages/[...slug].astro +90 -0
  211. package/apps/storefront/src/pages/admin.astro +1675 -0
  212. package/apps/storefront/src/pages/api/admin/orders/[id].ts +53 -0
  213. package/apps/storefront/src/pages/api/admin/orders.ts +71 -0
  214. package/apps/storefront/src/pages/api/auth/logout.ts +21 -0
  215. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +107 -0
  216. package/apps/storefront/src/pages/api/auth/send.ts +85 -0
  217. package/apps/storefront/src/pages/api/auth/verify.ts +135 -0
  218. package/apps/storefront/src/pages/api/cache-purge.ts +44 -0
  219. package/apps/storefront/src/pages/api/checkout-style.css.ts +43 -0
  220. package/apps/storefront/src/pages/api/cli/policy.ts +70 -0
  221. package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +164 -0
  222. package/apps/storefront/src/pages/api/csp-report.ts +25 -0
  223. package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +124 -0
  224. package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +120 -0
  225. package/apps/storefront/src/pages/api/dev/activity.ts +124 -0
  226. package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +73 -0
  227. package/apps/storefront/src/pages/api/dev/diff.ts +68 -0
  228. package/apps/storefront/src/pages/api/dev/file.ts +57 -0
  229. package/apps/storefront/src/pages/api/dev/locate.ts +125 -0
  230. package/apps/storefront/src/pages/api/dev/publish.ts +128 -0
  231. package/apps/storefront/src/pages/api/dev/tree.ts +70 -0
  232. package/apps/storefront/src/pages/api/dev/write.ts +116 -0
  233. package/apps/storefront/src/pages/api/newsletter.ts +104 -0
  234. package/apps/storefront/src/pages/api/request-access.ts +172 -0
  235. package/apps/storefront/src/pages/auth/login.astro +149 -0
  236. package/apps/storefront/src/pages/auth/magic.astro +105 -0
  237. package/apps/storefront/src/pages/capabilities.astro +292 -0
  238. package/apps/storefront/src/pages/collections/[handle].astro +177 -0
  239. package/apps/storefront/src/pages/collections/index.astro +59 -0
  240. package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +100 -0
  241. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +154 -0
  242. package/apps/storefront/src/pages/dashboard/index.astro +160 -0
  243. package/apps/storefront/src/pages/index.astro +290 -0
  244. package/apps/storefront/src/pages/llms.txt.ts +92 -0
  245. package/apps/storefront/src/pages/products/[handle].astro +402 -0
  246. package/apps/storefront/src/pages/robots.txt.ts +24 -0
  247. package/apps/storefront/src/pages/saved.astro +35 -0
  248. package/apps/storefront/src/pages/search.astro +150 -0
  249. package/apps/storefront/src/pages/sitemap.xml.ts +78 -0
  250. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +1280 -0
  251. package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +229 -0
  252. package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +144 -0
  253. package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +116 -0
  254. package/apps/storefront/src/pages/style-guide/index.astro +98 -0
  255. package/apps/storefront/src/pages/tenants/[id]/[...path].ts +57 -0
  256. package/apps/storefront/src/styles/global.css +292 -0
  257. package/apps/storefront/src/themes/reference.ts +68 -0
  258. package/apps/storefront/src/themes/schema.ts +111 -0
  259. package/apps/storefront/src/themes/tenants/index.ts +51 -0
  260. package/apps/storefront/tsconfig.json +16 -0
  261. package/apps/storefront/vitest.config.ts +22 -0
  262. package/package.json +26 -0
  263. package/packages/public-runtime/LICENSE +58 -0
  264. package/packages/public-runtime/package.json +22 -0
  265. package/packages/public-runtime/src/binary-path.ts +68 -0
  266. package/packages/public-runtime/src/capabilities.ts +89 -0
  267. package/packages/public-runtime/src/catalog-api.ts +81 -0
  268. package/packages/public-runtime/src/catalog-d1.ts +192 -0
  269. package/packages/public-runtime/src/checkout-style-publish.ts +200 -0
  270. package/packages/public-runtime/src/checkout-style.ts +375 -0
  271. package/packages/public-runtime/src/csp.ts +109 -0
  272. package/packages/public-runtime/src/customization-preview.ts +197 -0
  273. package/packages/public-runtime/src/customization-reconcile.ts +63 -0
  274. package/packages/public-runtime/src/customization-runtime.ts +157 -0
  275. package/packages/public-runtime/src/customization-scripts.ts +64 -0
  276. package/packages/public-runtime/src/customization-versioning.ts +95 -0
  277. package/packages/public-runtime/src/foxy-orders.ts +136 -0
  278. package/packages/public-runtime/src/foxy.ts +196 -0
  279. package/packages/public-runtime/src/hot-standby-readiness.ts +140 -0
  280. package/packages/public-runtime/src/index.ts +34 -0
  281. package/packages/public-runtime/src/product.ts +159 -0
  282. package/packages/public-runtime/src/search.ts +45 -0
  283. package/packages/public-runtime/src/tenant-assets.ts +444 -0
  284. package/packages/public-runtime/src/tenant.ts +384 -0
  285. package/packages/public-runtime/tsconfig.json +8 -0
  286. package/packages/public-runtime/vitest.config.ts +8 -0
  287. package/pnpm-workspace.yaml +7 -0
  288. package/scripts/build/copy-tenant-assets.mjs +69 -0
  289. package/scripts/dev/ai-edit.mjs +183 -0
  290. package/scripts/dev/checkout-watch.mjs +116 -0
  291. package/scripts/dev/file-browser.mjs +159 -0
  292. package/scripts/dev/file-writer.mjs +141 -0
  293. package/scripts/dev/git-status.mjs +171 -0
  294. package/scripts/dev/locate-handler.mjs +163 -0
  295. package/scripts/dev/locate.mjs +112 -0
  296. package/scripts/dev/port-check.mjs +48 -0
  297. package/scripts/dev/safe-path.mjs +109 -0
  298. package/scripts/dev/shot.mjs +46 -0
  299. package/scripts/dev/tenant-source-root.mjs +76 -0
  300. package/scripts/dev/unified-diff.mjs +81 -0
  301. package/scripts/tot-dev.mjs +376 -0
  302. package/tenants/README.md +14 -0
  303. package/tenants/home/content/chrome.json +7 -0
  304. package/tenants/home/content/home.html +58 -0
  305. package/tenants/home/content/home.json +11 -0
  306. package/tenants/home/public/js/storefront.js +210 -0
  307. package/tenants/home/public/logo-wordmark.png +0 -0
  308. package/tenants/home/public/pages/storefront.css +212 -0
  309. package/tenants/home/theme.json +6 -0
  310. package/tsconfig.base.json +20 -0
@@ -0,0 +1,366 @@
1
+ /**
2
+ * D1 catalog read-model queries (the one-store target — epics/edge-read-replica.md).
3
+ *
4
+ * Queries run against a small `Queryable` seam so the SAME code serves D1 in the
5
+ * Worker (via `fromD1`) and an in-memory `node:sqlite` in tests. Covers FTS5
6
+ * (trigram) search, getProductByHandle, collections, and faceted `listProducts`
7
+ * (filter + sort + paginate + facet counts in SQL GROUP BY) — the full read path,
8
+ * verified end-to-end against the real D1 engine (workerd).
9
+ */
10
+ import {
11
+ CATALOG_D1_SCHEMA,
12
+ toProductRow,
13
+ toCollectionRow,
14
+ facetRows,
15
+ collectionRows,
16
+ type CatalogCollection,
17
+ type CatalogProduct,
18
+ type Facet,
19
+ type ListProductsQuery,
20
+ type ListProductsResponse,
21
+ } from "@tot/public-runtime";
22
+ import { PRICE_BUCKETS } from "../priceBuckets.js";
23
+
24
+ /** Minimal async query seam — implemented by D1 and by the node:sqlite test shim. */
25
+ export interface Queryable {
26
+ all<T = Record<string, unknown>>(sql: string, ...params: unknown[]): Promise<T[]>;
27
+ first<T = Record<string, unknown>>(sql: string, ...params: unknown[]): Promise<T | null>;
28
+ run(sql: string, ...params: unknown[]): Promise<void>;
29
+ exec(sql: string): Promise<void>;
30
+ }
31
+
32
+ /** The slice of the Cloudflare D1Database we use (kept local to avoid a types dep). */
33
+ export interface D1Like {
34
+ prepare(sql: string): {
35
+ bind(...vals: unknown[]): {
36
+ all(): Promise<{ results: unknown[] }>;
37
+ first(): Promise<unknown>;
38
+ run(): Promise<unknown>;
39
+ };
40
+ };
41
+ exec(sql: string): Promise<unknown>;
42
+ }
43
+
44
+ /** Adapt a Cloudflare D1Database to the Queryable seam. */
45
+ export function fromD1(db: D1Like): Queryable {
46
+ return {
47
+ all: async <T>(sql: string, ...params: unknown[]) =>
48
+ (await db.prepare(sql).bind(...params).all()).results as T[],
49
+ first: async <T>(sql: string, ...params: unknown[]) =>
50
+ ((await db.prepare(sql).bind(...params).first()) ?? null) as T | null,
51
+ run: async (sql: string, ...params: unknown[]) => {
52
+ await db.prepare(sql).bind(...params).run();
53
+ },
54
+ exec: async (sql: string) => {
55
+ await db.exec(sql);
56
+ },
57
+ };
58
+ }
59
+
60
+ /** Create the catalog schema (idempotent). Used for local/test seeding. */
61
+ export async function initSchema(db: Queryable): Promise<void> {
62
+ await db.exec(CATALOG_D1_SCHEMA);
63
+ }
64
+
65
+ /** Insert one product into all read-model tables (products, fts, facets, collections). */
66
+ export async function insertProduct(db: Queryable, p: CatalogProduct, position = 0): Promise<void> {
67
+ const r = toProductRow(p, position);
68
+ await db.run(
69
+ `INSERT OR REPLACE INTO products
70
+ (handle,title,vendor,product_type,tags,description_text,price_min,price_max,currency,image,available,status,position,created_at,data)
71
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
72
+ r.handle, r.title, r.vendor, r.product_type, r.tags, r.description_text,
73
+ r.price_min, r.price_max, r.currency, r.image, r.available, r.status,
74
+ r.position, r.created_at, r.data,
75
+ );
76
+ await db.run(
77
+ `INSERT INTO products_fts (handle,title,vendor,product_type,tags,description_text)
78
+ VALUES (?,?,?,?,?,?)`,
79
+ p.handle, p.title, p.vendor ?? "", p.product_type ?? "", p.tags.join(" "), p.description_text,
80
+ );
81
+ for (const f of facetRows(p)) {
82
+ await db.run(`INSERT INTO product_facets (handle,name,value) VALUES (?,?,?)`, p.handle, f.name, f.value);
83
+ }
84
+ for (const c of collectionRows(p)) {
85
+ await db.run(`INSERT INTO product_collections (handle,collection) VALUES (?,?)`, p.handle, c.collection);
86
+ }
87
+ }
88
+
89
+ /** Insert one collection row. */
90
+ export async function insertCollection(db: Queryable, c: CatalogCollection): Promise<void> {
91
+ const r = toCollectionRow(c);
92
+ await db.run(
93
+ `INSERT OR REPLACE INTO collections (handle,title,position,data) VALUES (?,?,?,?)`,
94
+ r.handle, r.title, r.position, r.data,
95
+ );
96
+ }
97
+
98
+ export async function listCollections(db: Queryable): Promise<CatalogCollection[]> {
99
+ const rows = await db.all<{ data: string }>(`SELECT data FROM collections ORDER BY position ASC`);
100
+ return rows.map((r) => JSON.parse(r.data) as CatalogCollection);
101
+ }
102
+
103
+ export async function getCollectionByHandle(
104
+ db: Queryable,
105
+ handle: string,
106
+ ): Promise<CatalogCollection | null> {
107
+ const row = await db.first<{ data: string }>(
108
+ `SELECT data FROM collections WHERE handle = ?`,
109
+ handle,
110
+ );
111
+ return row ? (JSON.parse(row.data) as CatalogCollection) : null;
112
+ }
113
+
114
+ /** Build an FTS5 query expression: implicit-AND of quoted terms (trigram substring). */
115
+ function ftsExpr(q: string): string | null {
116
+ const terms = q
117
+ .trim()
118
+ .split(/\s+/)
119
+ .filter((t) => t.length >= 3) // FTS5 trigram tokens are 3 chars; shorter can't match
120
+ .map((t) => `"${t.replace(/"/g, '""')}"`);
121
+ return terms.length ? terms.join(" ") : null;
122
+ }
123
+
124
+ /** Typo-tolerant search over the active catalog, ranked by FTS5 relevance. */
125
+ export async function searchProducts(
126
+ db: Queryable,
127
+ q: string,
128
+ limit = 24,
129
+ ): Promise<CatalogProduct[]> {
130
+ const expr = ftsExpr(q);
131
+ if (!expr) return [];
132
+ const rows = await db.all<{ data: string }>(
133
+ `SELECT p.data AS data
134
+ FROM products_fts f
135
+ JOIN products p ON p.handle = f.handle
136
+ WHERE products_fts MATCH ? AND p.status = 'active'
137
+ ORDER BY rank
138
+ LIMIT ?`,
139
+ expr, limit,
140
+ );
141
+ return rows.map((r) => JSON.parse(r.data) as CatalogProduct);
142
+ }
143
+
144
+ /** Full product by handle (resolves drafts too — callers gate visibility). */
145
+ export async function getProductByHandle(
146
+ db: Queryable,
147
+ handle: string,
148
+ ): Promise<CatalogProduct | null> {
149
+ const row = await db.first<{ data: string }>(
150
+ `SELECT data FROM products WHERE handle = ?`,
151
+ handle,
152
+ );
153
+ return row ? (JSON.parse(row.data) as CatalogProduct) : null;
154
+ }
155
+
156
+ function sortClause(sort?: ListProductsQuery["sort"]): string {
157
+ switch (sort) {
158
+ case "price-asc":
159
+ return "ORDER BY p.price_min ASC, p.title COLLATE NOCASE ASC";
160
+ case "price-desc":
161
+ return "ORDER BY p.price_min DESC, p.title COLLATE NOCASE ASC";
162
+ case "newest":
163
+ return "ORDER BY p.created_at DESC";
164
+ case "title-asc":
165
+ return "ORDER BY p.title COLLATE NOCASE ASC";
166
+ case "featured":
167
+ default:
168
+ // Bestsellers float up, then the load order (position) — mirrors the
169
+ // in-Worker featured sort.
170
+ return `ORDER BY (CASE WHEN EXISTS (
171
+ SELECT 1 FROM product_facets f
172
+ WHERE f.handle = p.handle AND f.name = 'tags' AND f.value = 'bestseller'
173
+ ) THEN 0 ELSE 1 END), p.position ASC`;
174
+ }
175
+ }
176
+
177
+ /** One filter clause, tagged with the facet group it belongs to (so a
178
+ * per-group facet-count query can exclude just its own clause). "__q__" is
179
+ * the free-text search — never excluded, since it narrows every facet. */
180
+ interface FilterClause {
181
+ name: string;
182
+ sql: string;
183
+ params: unknown[];
184
+ }
185
+
186
+ function buildFilterClauses(query: ListProductsQuery): FilterClause[] {
187
+ const clauses: FilterClause[] = [];
188
+ for (const [name, values] of Object.entries(query.filters ?? {})) {
189
+ if (name === "availability" || name === "price" || !values.length) continue;
190
+ const placeholders = values.map(() => "?").join(",");
191
+ clauses.push({
192
+ name,
193
+ sql: `p.handle IN (SELECT handle FROM product_facets WHERE name = ? AND value IN (${placeholders}))`,
194
+ params: [name, ...values],
195
+ });
196
+ }
197
+ if (query.tags?.length) {
198
+ const placeholders = query.tags.map(() => "?").join(",");
199
+ clauses.push({
200
+ name: "tags",
201
+ sql: `p.handle IN (SELECT handle FROM product_facets WHERE name = 'tags' AND value IN (${placeholders}))`,
202
+ params: [...query.tags],
203
+ });
204
+ }
205
+ if (query.availability === "in-stock") {
206
+ clauses.push({ name: "availability", sql: "p.available = 1", params: [] });
207
+ }
208
+ if (query.priceMin != null) {
209
+ clauses.push({ name: "price", sql: "p.price_max >= ?", params: [query.priceMin] });
210
+ }
211
+ if (query.priceMax != null) {
212
+ clauses.push({ name: "price", sql: "p.price_min <= ?", params: [query.priceMax] });
213
+ }
214
+ if (query.q?.trim()) {
215
+ const like = `%${query.q.trim().toLowerCase()}%`;
216
+ clauses.push({
217
+ name: "__q__",
218
+ sql: "(lower(p.title) LIKE ? OR lower(p.vendor) LIKE ? OR lower(p.product_type) LIKE ? OR lower(p.description_text) LIKE ? OR lower(p.tags) LIKE ?)",
219
+ params: [like, like, like, like, like],
220
+ });
221
+ }
222
+ return clauses;
223
+ }
224
+
225
+ /** Render clauses (minus one excluded group, if given) as an " AND ..." suffix. */
226
+ function clauseSqlExcept(
227
+ clauses: FilterClause[],
228
+ except?: string,
229
+ ): { sql: string; params: unknown[] } {
230
+ const kept = clauses.filter((c) => c.name !== except);
231
+ return {
232
+ sql: kept.length ? ` AND ${kept.map((c) => c.sql).join(" AND ")}` : "",
233
+ params: kept.flatMap((c) => c.params),
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Facet counts with LIVE narrowing: each group's value counts apply every
239
+ * OTHER active filter (all groups but its own, plus q/availability/price),
240
+ * matching the storefront's multi-select facet UI. A value known to the pool
241
+ * but narrowed to zero by other filters still appears (count: 0) instead of
242
+ * vanishing, so the facet list stays stable while filtering.
243
+ */
244
+ async function buildFacetsSql(
245
+ db: Queryable,
246
+ poolSql: string,
247
+ poolParams: unknown[],
248
+ clauses: FilterClause[],
249
+ ): Promise<Facet[]> {
250
+ // Discover every (name, value) pair present anywhere in the pool
251
+ // (unfiltered), so facet groups/values don't disappear when narrowed to 0.
252
+ const allRows = await db.all<{ name: string; value: string }>(
253
+ `SELECT DISTINCT pf.name AS name, pf.value AS value
254
+ FROM product_facets pf
255
+ WHERE pf.handle IN (SELECT p.handle ${poolSql})`,
256
+ ...poolParams,
257
+ );
258
+ const namesInOrder: string[] = [];
259
+ const valuesByName = new Map<string, string[]>();
260
+ for (const r of allRows) {
261
+ if (!valuesByName.has(r.name)) {
262
+ namesInOrder.push(r.name);
263
+ valuesByName.set(r.name, []);
264
+ }
265
+ valuesByName.get(r.name)!.push(r.value);
266
+ }
267
+
268
+ const facets: Facet[] = [];
269
+ for (const name of namesInOrder) {
270
+ const { sql: exceptSql, params: exceptParams } = clauseSqlExcept(clauses, name);
271
+ const rows = await db.all<{ value: string; n: number }>(
272
+ `SELECT pf.value AS value, COUNT(DISTINCT pf.handle) AS n
273
+ FROM product_facets pf
274
+ WHERE pf.name = ?
275
+ AND pf.handle IN (SELECT p.handle ${poolSql}${exceptSql})
276
+ GROUP BY pf.value`,
277
+ name, ...poolParams, ...exceptParams,
278
+ );
279
+ const counts = new Map<string, number>(valuesByName.get(name)!.map((v) => [v, 0]));
280
+ for (const r of rows) counts.set(r.value, r.n);
281
+ facets.push({
282
+ name,
283
+ values: [...counts.entries()]
284
+ .map(([value, count]) => ({ value, count }))
285
+ .sort((a, b) => b.count - a.count || a.value.localeCompare(b.value)),
286
+ });
287
+ }
288
+
289
+ const { sql: availExceptSql, params: availExceptParams } = clauseSqlExcept(
290
+ clauses,
291
+ "availability",
292
+ );
293
+ const avail = await db.first<{ instock: number | null; total: number }>(
294
+ `SELECT SUM(CASE WHEN p.available = 1 THEN 1 ELSE 0 END) AS instock, COUNT(*) AS total ${poolSql}${availExceptSql}`,
295
+ ...poolParams, ...availExceptParams,
296
+ );
297
+ facets.push({
298
+ name: "availability",
299
+ values: [
300
+ { value: "in-stock", count: avail?.instock ?? 0 },
301
+ { value: "all", count: avail?.total ?? 0 },
302
+ ],
303
+ });
304
+
305
+ const { sql: priceExceptSql, params: priceExceptParams } = clauseSqlExcept(clauses, "price");
306
+ // Bucket boundaries are fixed numeric constants (PRICE_BUCKETS), not user
307
+ // input, so inlining them into the CASE expression is safe.
308
+ const bucketCase = PRICE_BUCKETS.map(
309
+ (b) =>
310
+ `SUM(CASE WHEN p.price_min >= ${b.min}${b.max != null ? ` AND p.price_min < ${b.max}` : ""} THEN 1 ELSE 0 END) AS "${b.key}"`,
311
+ ).join(", ");
312
+ const priceRow = await db.first<Record<string, number>>(
313
+ `SELECT ${bucketCase} ${poolSql}${priceExceptSql}`,
314
+ ...poolParams, ...priceExceptParams,
315
+ );
316
+ facets.push({
317
+ name: "price",
318
+ values: PRICE_BUCKETS.map((b) => ({ value: b.key, count: priceRow?.[b.key] ?? 0 })),
319
+ });
320
+
321
+ return facets;
322
+ }
323
+
324
+ /**
325
+ * Faceted listing — filter, sort, paginate, and count facets in SQL, replacing
326
+ * the in-Worker O(products×options×variants) scan. Option/tag filters are
327
+ * intra-group OR, inter-group AND (a product matches a group if it has any
328
+ * variant value in it). Facet counts are LIVE: each group's counts reflect
329
+ * every OTHER active filter (see buildFacetsSql), not just the collection pool.
330
+ */
331
+ export async function listProducts(
332
+ db: Queryable,
333
+ query: ListProductsQuery = {},
334
+ ): Promise<ListProductsResponse> {
335
+ const pageSize = Math.max(1, query.pageSize ?? 12);
336
+
337
+ // Pool: active products, optionally scoped to a collection.
338
+ const poolJoin = query.collection
339
+ ? "JOIN product_collections pc ON pc.handle = p.handle AND pc.collection = ?"
340
+ : "";
341
+ const poolParams: unknown[] = query.collection ? [query.collection] : [];
342
+ const poolSql = `FROM products p ${poolJoin} WHERE p.status = 'active'`;
343
+
344
+ // Result filters layered on the pool.
345
+ const clauses = buildFilterClauses(query);
346
+ const { sql: filterClause, params: filterParams } = clauseSqlExcept(clauses);
347
+ const resultParams = [...poolParams, ...filterParams];
348
+
349
+ const totalRow = await db.first<{ n: number }>(
350
+ `SELECT COUNT(*) AS n ${poolSql}${filterClause}`,
351
+ ...resultParams,
352
+ );
353
+ const total = totalRow?.n ?? 0;
354
+ const pageCount = Math.max(1, Math.ceil(total / pageSize));
355
+ const page = Math.min(Math.max(1, query.page ?? 1), pageCount);
356
+ const offset = (page - 1) * pageSize;
357
+
358
+ const rows = await db.all<{ data: string }>(
359
+ `SELECT p.data AS data ${poolSql}${filterClause} ${sortClause(query.sort)} LIMIT ? OFFSET ?`,
360
+ ...resultParams, pageSize, offset,
361
+ );
362
+ const products = rows.map((r) => JSON.parse(r.data) as CatalogProduct);
363
+
364
+ const facets = await buildFacetsSql(db, poolSql, poolParams, clauses);
365
+ return { products, page, pageSize, total, facets };
366
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Pure helpers for the ToT-staff two-phase admission (#8425 staff access), kept
3
+ * out of the Astro route files so the gate + selection-builder are unit-testable.
4
+ *
5
+ * PHASE 1 (admission gate): only a session that actually carries ToT-staff scope
6
+ * (`staff[]` non-empty) may search vendors or enter one. This is the near-side
7
+ * gate on the storefront's staff endpoints; tot20 re-checks staff scope + writes
8
+ * the audit row on the far side (`staffVendorAccess`), so this is defence-in-depth,
9
+ * not the authority.
10
+ *
11
+ * PHASE 2 (scope): after tot20 admits, we build a short-lived {@link StaffSelection}
12
+ * scoped to the returned staffRoles capability. It is stamped on the session as a
13
+ * DISTINCT marker from `tenants[]` — never a synthesized membership.
14
+ */
15
+ import type { SessionRecord, StaffSelection } from "@/lib/auth/session";
16
+
17
+ /**
18
+ * Staff selection lifetime. Deliberately short: staff cross-vendor access is a
19
+ * deliberate, audited act, not a standing grant — re-entering re-audits.
20
+ */
21
+ export const STAFF_SELECTION_TTL_SECONDS = 30 * 60;
22
+
23
+ /** True when the session carries ToT-staff scope (the admission-gate predicate). */
24
+ export function isStaffSession(
25
+ session: Pick<SessionRecord, "staff"> | null | undefined,
26
+ ): boolean {
27
+ return !!session && Array.isArray(session.staff) && session.staff.length > 0;
28
+ }
29
+
30
+ /** Build the short-lived staff selection marker to stamp on the session. */
31
+ export function buildStaffSelection(input: {
32
+ resource: string;
33
+ capability: string;
34
+ nowSeconds: number;
35
+ ttlSeconds?: number;
36
+ }): StaffSelection {
37
+ const ttl = input.ttlSeconds && input.ttlSeconds > 0 ? input.ttlSeconds : STAFF_SELECTION_TTL_SECONDS;
38
+ return {
39
+ resource: input.resource.trim(),
40
+ capability: input.capability,
41
+ exp: input.nowSeconds + ttl,
42
+ };
43
+ }