@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,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical approval state machine (baseline §Settings / §AI Workflows,
|
|
3
|
+
* ai-evidence-substrate item d). `settings.ts` and `aiWorkflows.ts` previously
|
|
4
|
+
* declared their own copies of this union; both now import it from here.
|
|
5
|
+
*/
|
|
6
|
+
export type ApprovalMode = "draft_only" | "approval_required" | "guarded_automation";
|
|
7
|
+
|
|
8
|
+
/** A `draft_only` recommendation must never be auto-applied; a human authors the action from it. */
|
|
9
|
+
export function isDraftOnly(mode: ApprovalMode): boolean {
|
|
10
|
+
return mode === "draft_only";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** An `approval_required` recommendation can be pre-drafted but needs an explicit human approval before it takes effect. */
|
|
14
|
+
export function requiresHumanApproval(mode: ApprovalMode): boolean {
|
|
15
|
+
return mode === "approval_required";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Only `guarded_automation` may execute without a human in the loop for that specific action. */
|
|
19
|
+
export function isAutomationAllowed(mode: ApprovalMode): boolean {
|
|
20
|
+
return mode === "guarded_automation";
|
|
21
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The floating `Ask` button's context envelope (baseline §Horizontal AI
|
|
3
|
+
* Concepts / Ask; ai-evidence-substrate item a). PURE + no I/O so it is
|
|
4
|
+
* unit-testable and reusable from both the inline admin.astro script (via a
|
|
5
|
+
* plain-object mirror, since `is:inline` scripts cannot import modules) and
|
|
6
|
+
* any future server-side Ask route. There is no chat backend yet — today
|
|
7
|
+
* this is emitted for dev inspection only (see admin.astro's `data-admin-ask`
|
|
8
|
+
* click handler), never sent over the network.
|
|
9
|
+
*/
|
|
10
|
+
import type { AdminTenantEnvelope } from "../envelope";
|
|
11
|
+
|
|
12
|
+
export type AdminRole = "internal_staff" | "merchant_operator";
|
|
13
|
+
|
|
14
|
+
export interface AskEnvelope {
|
|
15
|
+
tenant: AdminTenantEnvelope;
|
|
16
|
+
/** The active tab's `data-agent-scope` value, e.g. "orders". */
|
|
17
|
+
activeTab: string;
|
|
18
|
+
/** The currently selected row/detail record id, if the active tab has one selected. */
|
|
19
|
+
selectedRecordId: string | null;
|
|
20
|
+
role: AdminRole;
|
|
21
|
+
permissions: string[];
|
|
22
|
+
/** Active filters / saved view, if the active tab has any applied. */
|
|
23
|
+
filters: Record<string, string>;
|
|
24
|
+
/** Evidence reference ids (evidenceReference.ts) relevant to the current view/selection. */
|
|
25
|
+
evidenceContext: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const STAFF_PERMISSIONS = ["internal_admin", "cross_tenant_read"] as const;
|
|
29
|
+
const MERCHANT_PERMISSIONS = ["merchant_admin"] as const;
|
|
30
|
+
|
|
31
|
+
export function buildAskEnvelope(input: {
|
|
32
|
+
tenant: AdminTenantEnvelope;
|
|
33
|
+
isStaff: boolean;
|
|
34
|
+
activeTab: string;
|
|
35
|
+
selectedRecordId?: string | null;
|
|
36
|
+
filters?: Record<string, string>;
|
|
37
|
+
evidenceContext?: string[];
|
|
38
|
+
}): AskEnvelope {
|
|
39
|
+
return {
|
|
40
|
+
tenant: input.tenant,
|
|
41
|
+
activeTab: input.activeTab,
|
|
42
|
+
selectedRecordId: input.selectedRecordId ?? null,
|
|
43
|
+
role: input.isStaff ? "internal_staff" : "merchant_operator",
|
|
44
|
+
permissions: input.isStaff ? [...STAFF_PERMISSIONS] : [...MERCHANT_PERMISSIONS],
|
|
45
|
+
filters: input.filters ?? {},
|
|
46
|
+
evidenceContext: input.evidenceContext ?? [],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical audit event model (baseline "Audit logs must record both the
|
|
3
|
+
* operator and the target tenant"; ai-evidence-substrate item e). Every
|
|
4
|
+
* mutation-shaped or AI-suggested action in the admin console should record
|
|
5
|
+
* one of these, keyed by `targetTenant` — never `adminAppTenant`, per
|
|
6
|
+
* decision `tenant-envelope-contract`.
|
|
7
|
+
*/
|
|
8
|
+
export interface AuditEvent {
|
|
9
|
+
/** The signed-in operator's email, or a system actor id for automation-triggered events. */
|
|
10
|
+
operator: string;
|
|
11
|
+
targetTenant: string;
|
|
12
|
+
action: string;
|
|
13
|
+
at: string;
|
|
14
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { ApprovalMode } from "./approvalMode";
|
|
2
|
+
export { isDraftOnly, requiresHumanApproval, isAutomationAllowed } from "./approvalMode";
|
|
3
|
+
export type { AuditEvent } from "./audit";
|
|
4
|
+
export type { AiRecommendation } from "./recommendation";
|
|
5
|
+
export type { AskEnvelope, AdminRole } from "./askEnvelope";
|
|
6
|
+
export { buildAskEnvelope } from "./askEnvelope";
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Makes 3 of the 8 scaffolded AI Workflows (ai-workflows-scaffold) run
|
|
3
|
+
* end-to-end against the mock source domains (baseline §AI Workflows +
|
|
4
|
+
* §Suggested Buildout Sequence step 11). Still NO external side effects —
|
|
5
|
+
* "execution" here means producing an evidence-backed, approval-gated
|
|
6
|
+
* AiRecommendation plus a deterministic run history, never a real mutation.
|
|
7
|
+
* The other 5 workflows remain scaffold-only until their own units connect
|
|
8
|
+
* real source data.
|
|
9
|
+
*/
|
|
10
|
+
import type { AdminTenantEnvelope } from "../envelope";
|
|
11
|
+
import { getOrdersViewModel, getProductsViewModel, getEvidenceViewModel } from "../services/mock";
|
|
12
|
+
import { MOCK_ANCHOR_ISO, offsetIso, randomInt, seededRandom } from "../seed";
|
|
13
|
+
import { OPERATIONAL_WORKFLOW_KEYS, type OperationalWorkflowKey } from "../viewmodels/aiWorkflows";
|
|
14
|
+
import type { ApprovalMode } from "./approvalMode";
|
|
15
|
+
import type { AuditEvent } from "./audit";
|
|
16
|
+
import type { AiRecommendation } from "./recommendation";
|
|
17
|
+
|
|
18
|
+
export { OPERATIONAL_WORKFLOW_KEYS, type OperationalWorkflowKey };
|
|
19
|
+
|
|
20
|
+
export interface WorkflowRunResult {
|
|
21
|
+
recommendation: AiRecommendation;
|
|
22
|
+
runHistory: AuditEvent[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildRunHistory(input: {
|
|
26
|
+
targetTenant: string;
|
|
27
|
+
runKey: string;
|
|
28
|
+
count: number;
|
|
29
|
+
action: string;
|
|
30
|
+
}): AuditEvent[] {
|
|
31
|
+
const rng = seededRandom(`workflow-run:${input.runKey}:${input.targetTenant}`);
|
|
32
|
+
const history: AuditEvent[] = [];
|
|
33
|
+
let at = MOCK_ANCHOR_ISO;
|
|
34
|
+
for (let i = 0; i < input.count; i += 1) {
|
|
35
|
+
at = offsetIso(at, -randomInt(rng, 300, 2400));
|
|
36
|
+
history.push({ operator: "system:scheduler", targetTenant: input.targetTenant, action: input.action, at });
|
|
37
|
+
}
|
|
38
|
+
return history;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function runStockoutForecaster(envelope: AdminTenantEnvelope, approvalMode: ApprovalMode): WorkflowRunResult {
|
|
42
|
+
const products = getProductsViewModel(envelope);
|
|
43
|
+
const LOW_STOCK_THRESHOLD = 20;
|
|
44
|
+
const lowStock = products.rows
|
|
45
|
+
.flatMap((row) => {
|
|
46
|
+
const detail = products.detailsById[row.id]!;
|
|
47
|
+
return detail.variants
|
|
48
|
+
.filter((v) => v.inventory < LOW_STOCK_THRESHOLD)
|
|
49
|
+
.map((v) => ({ product: row, variant: v }));
|
|
50
|
+
})
|
|
51
|
+
.sort((a, b) => a.variant.inventory - b.variant.inventory);
|
|
52
|
+
|
|
53
|
+
const evidenceRefs = lowStock.map(({ product }) => `evidence:${envelope.targetTenant}:product:${product.id}`);
|
|
54
|
+
const summary =
|
|
55
|
+
lowStock.length === 0
|
|
56
|
+
? "No variants below the reorder threshold — nothing to forecast this run."
|
|
57
|
+
: `${lowStock.length} variant(s) below ${LOW_STOCK_THRESHOLD} units, lowest: ${lowStock[0]!.variant.name} (${lowStock[0]!.product.name}) at ${lowStock[0]!.variant.inventory}.`;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
recommendation: {
|
|
61
|
+
summary,
|
|
62
|
+
confidence: lowStock.length > 0 ? 0.8 : 0.99,
|
|
63
|
+
evidenceRefs: [...new Set(evidenceRefs)],
|
|
64
|
+
owner: "Ops",
|
|
65
|
+
approvalMode,
|
|
66
|
+
auditTrail: [],
|
|
67
|
+
},
|
|
68
|
+
runHistory: buildRunHistory({
|
|
69
|
+
targetTenant: envelope.targetTenant,
|
|
70
|
+
runKey: "stockout-forecaster",
|
|
71
|
+
count: 3,
|
|
72
|
+
action: "Scanned product variant inventory for reorder thresholds",
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function runChargebackDefenseWatcher(envelope: AdminTenantEnvelope, approvalMode: ApprovalMode): WorkflowRunResult {
|
|
78
|
+
const orders = getOrdersViewModel(envelope);
|
|
79
|
+
const evidenceGraph = getEvidenceViewModel(envelope);
|
|
80
|
+
const holds = orders.rows.filter((r) => r.shipState === "hold_review_required");
|
|
81
|
+
|
|
82
|
+
const evidenceRefs = holds.flatMap((row) => {
|
|
83
|
+
const orderRefId = `evidence:${envelope.targetTenant}:order:${row.id}`;
|
|
84
|
+
const orderRef = evidenceGraph.find((e) => e.id === orderRefId);
|
|
85
|
+
return orderRef ? [orderRef.id, ...orderRef.relatedRefs] : [orderRefId];
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const summary =
|
|
89
|
+
holds.length === 0
|
|
90
|
+
? "No orders on hold — no dispute-evidence packets needed this run."
|
|
91
|
+
: `${holds.length} order(s) on hold need evidence assembled before the dispute window: ${holds.map((r) => r.id).join(", ")}.`;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
recommendation: {
|
|
95
|
+
summary,
|
|
96
|
+
confidence: holds.length > 0 ? 0.65 : 0.99,
|
|
97
|
+
evidenceRefs: [...new Set(evidenceRefs)],
|
|
98
|
+
owner: "Risk",
|
|
99
|
+
approvalMode,
|
|
100
|
+
auditTrail: [],
|
|
101
|
+
},
|
|
102
|
+
runHistory: buildRunHistory({
|
|
103
|
+
targetTenant: envelope.targetTenant,
|
|
104
|
+
runKey: "chargeback-defense-watcher",
|
|
105
|
+
count: 4,
|
|
106
|
+
action: "Checked held orders for missing dispute evidence",
|
|
107
|
+
}),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function runAiChannelReadinessAudit(envelope: AdminTenantEnvelope, approvalMode: ApprovalMode): WorkflowRunResult {
|
|
112
|
+
const products = getProductsViewModel(envelope);
|
|
113
|
+
const notReady = products.rows.filter((r) => !r.aiChannelReady);
|
|
114
|
+
const evidenceRefs = notReady.map((row) => `evidence:${envelope.targetTenant}:product:${row.id}`);
|
|
115
|
+
|
|
116
|
+
const summary =
|
|
117
|
+
notReady.length === 0
|
|
118
|
+
? "Every product is AI channel ready — nothing to audit this run."
|
|
119
|
+
: `${notReady.length} of ${products.rows.length} products are not AI channel ready, blocked by ${notReady.reduce((sum, r) => sum + r.missingExciseCount, 0)} total excise metadata gaps.`;
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
recommendation: {
|
|
123
|
+
summary,
|
|
124
|
+
confidence: notReady.length > 0 ? 0.75 : 0.99,
|
|
125
|
+
evidenceRefs: [...new Set(evidenceRefs)],
|
|
126
|
+
owner: "Products",
|
|
127
|
+
approvalMode,
|
|
128
|
+
auditTrail: [],
|
|
129
|
+
},
|
|
130
|
+
runHistory: buildRunHistory({
|
|
131
|
+
targetTenant: envelope.targetTenant,
|
|
132
|
+
runKey: "ai-channel-readiness-audit",
|
|
133
|
+
count: 2,
|
|
134
|
+
action: "Audited catalog for AI channel readiness gaps",
|
|
135
|
+
}),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const RUNNERS: Record<OperationalWorkflowKey, (envelope: AdminTenantEnvelope, approvalMode: ApprovalMode) => WorkflowRunResult> = {
|
|
140
|
+
"stockout-forecaster": runStockoutForecaster,
|
|
141
|
+
"chargeback-defense-watcher": runChargebackDefenseWatcher,
|
|
142
|
+
"ai-channel-readiness-audit": runAiChannelReadinessAudit,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export function isOperational(key: string): key is OperationalWorkflowKey {
|
|
146
|
+
return (OPERATIONAL_WORKFLOW_KEYS as readonly string[]).includes(key);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function runOperationalWorkflow(
|
|
150
|
+
key: OperationalWorkflowKey,
|
|
151
|
+
envelope: AdminTenantEnvelope,
|
|
152
|
+
approvalMode: ApprovalMode,
|
|
153
|
+
): WorkflowRunResult {
|
|
154
|
+
return RUNNERS[key](envelope, approvalMode);
|
|
155
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared shape every AI-suggested action in the admin console carries
|
|
3
|
+
* (baseline: "Evidence, confidence, owner, approval mode, and audit history
|
|
4
|
+
* attached to recommendations"). `WorkflowRecord.suggestedAction` uses this;
|
|
5
|
+
* later units (Order Assist, Fulfillment Assist, etc.) should reuse it rather
|
|
6
|
+
* than inventing a parallel "suggestion" shape per tab.
|
|
7
|
+
*/
|
|
8
|
+
import type { ApprovalMode } from "./approvalMode";
|
|
9
|
+
import type { AuditEvent } from "./audit";
|
|
10
|
+
|
|
11
|
+
export interface AiRecommendation {
|
|
12
|
+
summary: string;
|
|
13
|
+
/** 0–1 confidence the recommendation is correct/safe to act on. */
|
|
14
|
+
confidence: number;
|
|
15
|
+
/** Ids into the evidence reference model (evidenceReference.ts) backing this recommendation. */
|
|
16
|
+
evidenceRefs: string[];
|
|
17
|
+
owner: string;
|
|
18
|
+
approvalMode: ApprovalMode;
|
|
19
|
+
auditTrail: AuditEvent[];
|
|
20
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate + sanitize merchant-submitted checkout styling into a safe, complete
|
|
3
|
+
* `CheckoutStyle` before it is published to the versioned spine (U12 save path).
|
|
4
|
+
*
|
|
5
|
+
* The submitted body is a partial (`DeepPartial<CheckoutStyle>`): a merchant may
|
|
6
|
+
* set one field without restating the rest, so this merges any valid override
|
|
7
|
+
* over `DEFAULT_CHECKOUT_STYLE` (reusing U10's `mergeCheckoutStyle`) and returns a
|
|
8
|
+
* complete descriptor. It is the WRITE-side guard; the go-live GateReport (U13)
|
|
9
|
+
* still does the deeper compliance/contrast/perf pass on promote.
|
|
10
|
+
*
|
|
11
|
+
* Two hard constraints enforced here (both from the epic's Foxy-theme reality):
|
|
12
|
+
* - **ASCII-safe.** Foxy's checkout cache pass is not Unicode-safe — a non-ASCII
|
|
13
|
+
* byte in a value that U11 renders into the pushed CSS can blank the checkout.
|
|
14
|
+
* Every provided string leaf must be printable ASCII.
|
|
15
|
+
* - **CSS-token hygiene.** These values are interpolated into generated CSS
|
|
16
|
+
* (U11), so we reject the characters that could break out of a declaration
|
|
17
|
+
* (`;` `{` `}` `<` `>` and newlines). Defense-in-depth, not the full gate.
|
|
18
|
+
*
|
|
19
|
+
* Pure + dependency-free (no I/O, no Astro): unit-testable in isolation and safe
|
|
20
|
+
* to reuse from the gate (U13) or the CSS renderer (U11) if promoted to
|
|
21
|
+
* public-runtime later.
|
|
22
|
+
*/
|
|
23
|
+
import {
|
|
24
|
+
DEFAULT_CHECKOUT_STYLE,
|
|
25
|
+
mergeCheckoutStyle,
|
|
26
|
+
type CheckoutStyle,
|
|
27
|
+
type CheckoutStyleColors,
|
|
28
|
+
} from "@tot/public-runtime";
|
|
29
|
+
|
|
30
|
+
export type CheckoutStyleInputResult =
|
|
31
|
+
| { ok: true; style: CheckoutStyle }
|
|
32
|
+
| { ok: false; errors: string[] };
|
|
33
|
+
|
|
34
|
+
/** Per-field length ceilings — generous enough for real stacks, tight enough to
|
|
35
|
+
* bound what gets pushed to Foxy. */
|
|
36
|
+
const MAX = {
|
|
37
|
+
color: 64,
|
|
38
|
+
fontFamily: 200,
|
|
39
|
+
length: 16,
|
|
40
|
+
logoUrl: 500,
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
const COLOR_KEYS: (keyof CheckoutStyleColors)[] = [
|
|
44
|
+
"background",
|
|
45
|
+
"surface",
|
|
46
|
+
"primary",
|
|
47
|
+
"primaryContrast",
|
|
48
|
+
"accent",
|
|
49
|
+
"text",
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/** Printable ASCII only (0x20–0x7E). Rejects control chars, newlines, and any
|
|
53
|
+
* non-ASCII (the Foxy-cache Unicode hazard). */
|
|
54
|
+
function isPrintableAscii(value: string): boolean {
|
|
55
|
+
return /^[\x20-\x7E]*$/.test(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** CSS-value hygiene: no declaration/selector-breaking characters. */
|
|
59
|
+
function hasUnsafeCssChars(value: string): boolean {
|
|
60
|
+
return /[;{}<>]/.test(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validate one string leaf: must be a string, printable ASCII, within `max`, free
|
|
65
|
+
* of CSS-breaking characters, and (unless `allowEmpty`) non-empty after trim.
|
|
66
|
+
* Returns the trimmed value or pushes a labeled error.
|
|
67
|
+
*/
|
|
68
|
+
function takeString(
|
|
69
|
+
raw: unknown,
|
|
70
|
+
label: string,
|
|
71
|
+
max: number,
|
|
72
|
+
errors: string[],
|
|
73
|
+
allowEmpty = false,
|
|
74
|
+
): string | undefined {
|
|
75
|
+
if (raw === undefined) return undefined;
|
|
76
|
+
if (typeof raw !== "string") {
|
|
77
|
+
errors.push(`${label} must be a string`);
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
const value = raw.trim();
|
|
81
|
+
if (!value) {
|
|
82
|
+
if (allowEmpty) return "";
|
|
83
|
+
errors.push(`${label} must not be empty`);
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
if (value.length > max) {
|
|
87
|
+
errors.push(`${label} must be at most ${max} characters`);
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
if (!isPrintableAscii(value)) {
|
|
91
|
+
errors.push(`${label} must be printable ASCII (Foxy checkout is not Unicode-safe)`);
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
if (hasUnsafeCssChars(value)) {
|
|
95
|
+
errors.push(`${label} must not contain ; { } < or >`);
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function asObject(raw: unknown): Record<string, unknown> | null {
|
|
102
|
+
return raw && typeof raw === "object" && !Array.isArray(raw)
|
|
103
|
+
? (raw as Record<string, unknown>)
|
|
104
|
+
: null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Validate + sanitize an untrusted checkout-style body into a complete
|
|
109
|
+
* `CheckoutStyle`. On any field error, returns every error (not just the first)
|
|
110
|
+
* so the editor can surface them together. On success, the returned style is the
|
|
111
|
+
* default with the validated overrides merged on top.
|
|
112
|
+
*/
|
|
113
|
+
export function validateCheckoutStyleInput(raw: unknown): CheckoutStyleInputResult {
|
|
114
|
+
const body = asObject(raw);
|
|
115
|
+
if (!body) return { ok: false, errors: ["body must be a JSON object"] };
|
|
116
|
+
|
|
117
|
+
const errors: string[] = [];
|
|
118
|
+
const override: {
|
|
119
|
+
colors?: Partial<CheckoutStyleColors>;
|
|
120
|
+
typography?: { fontFamily?: string; baseSize?: string };
|
|
121
|
+
button?: { radius?: string };
|
|
122
|
+
logoUrl?: string;
|
|
123
|
+
} = {};
|
|
124
|
+
|
|
125
|
+
const colors = asObject(body.colors);
|
|
126
|
+
if (body.colors !== undefined && !colors) {
|
|
127
|
+
errors.push("colors must be an object");
|
|
128
|
+
} else if (colors) {
|
|
129
|
+
const out: Partial<CheckoutStyleColors> = {};
|
|
130
|
+
for (const key of COLOR_KEYS) {
|
|
131
|
+
const v = takeString(colors[key], `colors.${key}`, MAX.color, errors);
|
|
132
|
+
if (v !== undefined) out[key] = v;
|
|
133
|
+
}
|
|
134
|
+
if (Object.keys(out).length > 0) override.colors = out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const typography = asObject(body.typography);
|
|
138
|
+
if (body.typography !== undefined && !typography) {
|
|
139
|
+
errors.push("typography must be an object");
|
|
140
|
+
} else if (typography) {
|
|
141
|
+
const out: { fontFamily?: string; baseSize?: string } = {};
|
|
142
|
+
const fontFamily = takeString(typography.fontFamily, "typography.fontFamily", MAX.fontFamily, errors);
|
|
143
|
+
if (fontFamily !== undefined) out.fontFamily = fontFamily;
|
|
144
|
+
const baseSize = takeString(typography.baseSize, "typography.baseSize", MAX.length, errors);
|
|
145
|
+
if (baseSize !== undefined) out.baseSize = baseSize;
|
|
146
|
+
if (Object.keys(out).length > 0) override.typography = out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const button = asObject(body.button);
|
|
150
|
+
if (body.button !== undefined && !button) {
|
|
151
|
+
errors.push("button must be an object");
|
|
152
|
+
} else if (button) {
|
|
153
|
+
const radius = takeString(button.radius, "button.radius", MAX.length, errors);
|
|
154
|
+
if (radius !== undefined) override.button = { radius };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// logoUrl: optional. An empty string clears it (falls back to default = none);
|
|
158
|
+
// a set value must be an absolute https URL within the length cap.
|
|
159
|
+
if (body.logoUrl !== undefined) {
|
|
160
|
+
const logo = takeString(body.logoUrl, "logoUrl", MAX.logoUrl, errors, true);
|
|
161
|
+
if (logo) {
|
|
162
|
+
if (!/^https:\/\/\S+$/.test(logo)) {
|
|
163
|
+
errors.push("logoUrl must be an absolute https URL");
|
|
164
|
+
} else {
|
|
165
|
+
override.logoUrl = logo;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (errors.length > 0) return { ok: false, errors };
|
|
171
|
+
return { ok: true, style: mergeCheckoutStyle(DEFAULT_CHECKOUT_STYLE, override) };
|
|
172
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide which tenant a checkout-style save may write to (U12b — staff
|
|
3
|
+
* cross-tenant save).
|
|
4
|
+
*
|
|
5
|
+
* The base save gate (`resolveOwnerSession`) resolves the operator's OWN tenant
|
|
6
|
+
* (the routed appDomain). U12 always wrote there. But a ToT-staff operator can
|
|
7
|
+
* "operate as" another tenant via the admin picker (`?asTenant=`), and their
|
|
8
|
+
* save must land on THAT tenant's artifact — provided they're actually
|
|
9
|
+
* authorized for it. This resolver makes that decision, and it deliberately
|
|
10
|
+
* reuses `resolveAdminTenantMode` — the SAME staff-membership/admission gate the
|
|
11
|
+
* admin shell renders the picker envelope from — so there is ONE authorization
|
|
12
|
+
* model, not a parallel one.
|
|
13
|
+
*
|
|
14
|
+
* Deny-by-default: a requested target that differs from the owner is honored only
|
|
15
|
+
* when `resolveAdminTenantMode` authorizes it (staff + a real membership or a live
|
|
16
|
+
* staff selection for that exact tenant). The merchant / own-tenant path is
|
|
17
|
+
* unchanged: no target (or a target equal to the owner) writes to the owner, with
|
|
18
|
+
* no session read required.
|
|
19
|
+
*
|
|
20
|
+
* Pure (no I/O, no Astro) — the caller supplies the already-read viewer session —
|
|
21
|
+
* so it is unit-testable in isolation.
|
|
22
|
+
*/
|
|
23
|
+
import { resolveAdminTenantMode } from "@/lib/adminTenantEnvelope";
|
|
24
|
+
import type { SessionRecord } from "@/lib/auth/session";
|
|
25
|
+
|
|
26
|
+
export type WriteTargetResult =
|
|
27
|
+
| { ok: true; targetTenant: string }
|
|
28
|
+
| { ok: false; status: number; message: string };
|
|
29
|
+
|
|
30
|
+
export function resolveCheckoutStyleWriteTarget(input: {
|
|
31
|
+
/** The operator's own tenant appDomain (from resolveOwnerSession = routed tenant). */
|
|
32
|
+
owner: string;
|
|
33
|
+
/** The tenant the caller asked to write, from the request body (the picker target). */
|
|
34
|
+
requestedTarget: string | null | undefined;
|
|
35
|
+
/** The signed-in viewer session — only needed (and only read) for a cross-tenant write. */
|
|
36
|
+
session: SessionRecord | null | undefined;
|
|
37
|
+
/** Current time (epoch seconds), for the staff-selection expiry check. */
|
|
38
|
+
nowSeconds: number;
|
|
39
|
+
}): WriteTargetResult {
|
|
40
|
+
const requested = (input.requestedTarget ?? "").trim();
|
|
41
|
+
|
|
42
|
+
// Merchant / own-tenant path — unchanged. No requested target, or a target that
|
|
43
|
+
// is just the operator's own tenant, writes to the owner.
|
|
44
|
+
if (!requested || requested === input.owner) {
|
|
45
|
+
return { ok: true, targetTenant: input.owner };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Cross-tenant: authorize EXACTLY as the admin shell does. adminAppTenant is the
|
|
49
|
+
// routed tenant (the owner); a staff session with a real membership or a live
|
|
50
|
+
// staff selection for `requested` yields envelope.targetTenant === requested.
|
|
51
|
+
const mode = resolveAdminTenantMode({
|
|
52
|
+
session: input.session,
|
|
53
|
+
adminAppTenant: input.owner,
|
|
54
|
+
requestedTarget: requested,
|
|
55
|
+
nowSeconds: input.nowSeconds,
|
|
56
|
+
});
|
|
57
|
+
if (mode.deniedTarget || mode.envelope.targetTenant !== requested) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
status: 403,
|
|
61
|
+
message: `not authorized to write checkout styling for tenant: ${requested}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, targetTenant: requested };
|
|
65
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical tenant envelope for the admin console lives in
|
|
3
|
+
* `lib/adminTenantEnvelope.ts` (admin-tenant-mode) — it carries the real
|
|
4
|
+
* staff-override authorization (`resolveAdminTenantMode`) that admin.astro
|
|
5
|
+
* actually runs. This module re-exports that type so every mock service under
|
|
6
|
+
* `lib/admin/**` can keep importing it from a stable relative path
|
|
7
|
+
* (`../../envelope`) without declaring a second, competing interface (decision
|
|
8
|
+
* `envelope-type-duplication-follow-up`).
|
|
9
|
+
*/
|
|
10
|
+
export type { AdminTenantEnvelope } from "@/lib/adminTenantEnvelope";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence reference model (baseline "Evidence Graph"; ai-evidence-substrate
|
|
3
|
+
* item c). A durable, typed pointer connecting the domains the baseline names:
|
|
4
|
+
* orders, customers, products, payments, verification events,
|
|
5
|
+
* fulfillment/tracking, support/contact events, policy acceptance, tax/excise
|
|
6
|
+
* evidence, and admin/user actions. `relatedRefs` is how a single reference
|
|
7
|
+
* connects across domains (e.g. an order's evidence pointing at the
|
|
8
|
+
* verification event and payment that backed it) — the "graph" edges.
|
|
9
|
+
*/
|
|
10
|
+
export type EvidenceKind =
|
|
11
|
+
| "order"
|
|
12
|
+
| "customer"
|
|
13
|
+
| "product"
|
|
14
|
+
| "payment"
|
|
15
|
+
| "verification_event"
|
|
16
|
+
| "fulfillment_tracking"
|
|
17
|
+
| "support_contact_event"
|
|
18
|
+
| "policy_acceptance"
|
|
19
|
+
| "tax_excise"
|
|
20
|
+
| "admin_action";
|
|
21
|
+
|
|
22
|
+
export interface EvidenceReference {
|
|
23
|
+
/** Stable id for this reference, e.g. `evidence:order:#2248293823`. */
|
|
24
|
+
id: string;
|
|
25
|
+
kind: EvidenceKind;
|
|
26
|
+
/** The domain record this points at (an order id, customer id, etc.). */
|
|
27
|
+
recordId: string;
|
|
28
|
+
label: string;
|
|
29
|
+
at: string;
|
|
30
|
+
/** Other evidence reference ids this one connects to. */
|
|
31
|
+
relatedRefs: string[];
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AdminTenantEnvelope } from "./envelope";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-request memoization for the pure mock view-model builders. Each builder is
|
|
5
|
+
* a pure function of `envelope.targetTenant`, but admin.astro mounts every tab on
|
|
6
|
+
* every request and several builders call one another (e.g. evidence rebuilds
|
|
7
|
+
* orders/customers/products), so a single render can rebuild the same tenant's
|
|
8
|
+
* dataset many times.
|
|
9
|
+
*
|
|
10
|
+
* The cache is keyed by the envelope OBJECT REFERENCE via a WeakMap: admin.astro
|
|
11
|
+
* constructs one envelope per request and passes that same reference to every
|
|
12
|
+
* tab, so the cache is naturally scoped to a single request and garbage-collected
|
|
13
|
+
* with the envelope. That avoids the cross-request leak a naive module-level
|
|
14
|
+
* tenant-keyed Map would cause on a warm Cloudflare Worker isolate — no manual
|
|
15
|
+
* eviction, no unbounded growth, no stale cross-tenant reuse.
|
|
16
|
+
*/
|
|
17
|
+
export function memoizeByEnvelope<T>(
|
|
18
|
+
compute: (envelope: AdminTenantEnvelope) => T,
|
|
19
|
+
): (envelope: AdminTenantEnvelope) => T {
|
|
20
|
+
const cache = new WeakMap<AdminTenantEnvelope, T>();
|
|
21
|
+
return (envelope) => {
|
|
22
|
+
if (cache.has(envelope)) return cache.get(envelope) as T;
|
|
23
|
+
const value = compute(envelope);
|
|
24
|
+
cache.set(envelope, value);
|
|
25
|
+
return value;
|
|
26
|
+
};
|
|
27
|
+
}
|