@tokenoftrust/storefront-runner 1.3.4-rc.4 → 1.4.0
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/apps/storefront/astro.config.mjs +15 -0
- package/apps/storefront/dev-plugins/dev-publish.mjs +55 -0
- package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +86 -7
- package/apps/storefront/drizzle.config.apps.ts +13 -0
- package/apps/storefront/env.d.ts +10 -0
- package/apps/storefront/migrations/README.md +13 -7
- package/apps/storefront/migrations-apps/0000_fast_millenium_guard.sql +129 -0
- package/apps/storefront/migrations-apps/meta/0000_snapshot.json +843 -0
- package/apps/storefront/migrations-apps/meta/_journal.json +13 -0
- package/apps/storefront/package.json +13 -1
- package/apps/storefront/perf/README.md +64 -0
- package/apps/storefront/perf/assert-budgets.ts +159 -0
- package/apps/storefront/playwright.config.ts +23 -0
- package/apps/storefront/public/js/dashboard-apps.js +173 -0
- package/apps/storefront/public/shared/commerce-marketing.css +221 -0
- package/apps/storefront/src/components/CollectionCard.astro +1 -0
- package/apps/storefront/src/components/ProductCard.astro +1 -0
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +1959 -0
- package/apps/storefront/src/components/apps/AppWidgetFrame.astro +30 -0
- package/apps/storefront/src/components/chrome/NavDropdown.astro +6 -3
- package/apps/storefront/src/components/chrome/SiteFooter.astro +10 -0
- package/apps/storefront/src/components/chrome/SiteHeader.astro +10 -0
- package/apps/storefront/src/components/commerce/RatingStars.astro +3 -2
- package/apps/storefront/src/components/content/Callout.astro +75 -0
- package/apps/storefront/src/components/content/NeedsReviewCallout.astro +66 -0
- package/apps/storefront/src/components/content/ProseSections.astro +121 -0
- package/apps/storefront/src/components/content/ProseToc.astro +34 -0
- package/apps/storefront/src/components/content/RichText.astro +44 -0
- package/apps/storefront/src/components/content/TrustStrip.astro +46 -0
- package/apps/storefront/src/components/home/Hero.astro +14 -0
- package/apps/storefront/src/components/islands/CheckoutComplianceGate.tsx +295 -0
- package/apps/storefront/src/components/islands/ImageGallery.tsx +42 -21
- package/apps/storefront/src/components/islands/VariantSelector.tsx +62 -12
- package/apps/storefront/src/components/plp/FacetSidebar.astro +2 -2
- package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +1 -0
- package/apps/storefront/src/config/compliance/rulesets.ts +79 -0
- package/apps/storefront/src/config/storeName.ts +34 -0
- package/apps/storefront/src/layouts/Layout.astro +97 -23
- package/apps/storefront/src/lib/analytics/budgets.json +69 -0
- package/apps/storefront/src/lib/analytics/lighthouseReport.ts +109 -0
- package/apps/storefront/src/lib/analytics/perfBudgets.ts +452 -0
- package/apps/storefront/src/lib/analytics/rumAlert.ts +179 -0
- package/apps/storefront/src/lib/analytics/webVitals.ts +269 -0
- package/apps/storefront/src/lib/apps/adminService.ts +106 -0
- package/apps/storefront/src/lib/apps/adminSession.ts +205 -0
- package/apps/storefront/src/lib/apps/apiAuth.ts +91 -0
- package/apps/storefront/src/lib/apps/apiRoute.ts +35 -0
- package/apps/storefront/src/lib/apps/catalogMapper.ts +39 -0
- package/apps/storefront/src/lib/apps/credentials.ts +153 -0
- package/apps/storefront/src/lib/apps/gatewayKeys.ts +156 -0
- package/apps/storefront/src/lib/apps/healthAggregate.ts +66 -0
- package/apps/storefront/src/lib/apps/orders/attributionService.ts +206 -0
- package/apps/storefront/src/lib/apps/orders/customerHash.ts +28 -0
- package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +203 -0
- package/apps/storefront/src/lib/apps/orders/idempotency.ts +100 -0
- package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +117 -0
- package/apps/storefront/src/lib/apps/orders/orderMapper.ts +91 -0
- package/apps/storefront/src/lib/apps/orders/ordersStore.ts +181 -0
- package/apps/storefront/src/lib/apps/registryService.ts +579 -0
- package/apps/storefront/src/lib/apps/scopes.ts +79 -0
- package/apps/storefront/src/lib/apps/tokenIssuer.ts +121 -0
- package/apps/storefront/src/lib/apps/tokenVerifier.ts +148 -0
- package/apps/storefront/src/lib/apps/widgets/eligibility.ts +18 -0
- package/apps/storefront/src/lib/apps/widgets/frameProps.ts +52 -0
- package/apps/storefront/src/lib/apps/widgets/launchToken.ts +84 -0
- package/apps/storefront/src/lib/apps/widgets/placements.ts +57 -0
- package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +111 -0
- package/apps/storefront/src/lib/auth/adminEntry.ts +119 -0
- package/apps/storefront/src/lib/auth/identityToken.ts +21 -2
- package/apps/storefront/src/lib/auth/loginGate.ts +102 -18
- package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +215 -0
- package/apps/storefront/src/lib/auth/route.ts +16 -1
- package/apps/storefront/src/lib/auth/session.ts +8 -0
- package/apps/storefront/src/lib/auth/stepUpChallenge.ts +107 -0
- package/apps/storefront/src/lib/auth/totAccessClient.ts +208 -0
- package/apps/storefront/src/lib/blog/provider.ts +39 -0
- package/apps/storefront/src/lib/blog/types.ts +26 -0
- package/apps/storefront/src/lib/checkoutCommerce.ts +39 -1
- package/apps/storefront/src/lib/chrome/model.ts +11 -0
- package/apps/storefront/src/lib/compliance/enforcement.ts +95 -0
- package/apps/storefront/src/lib/content/callout.ts +78 -0
- package/apps/storefront/src/lib/content/index.ts +27 -0
- package/apps/storefront/src/lib/content/needsReview.ts +58 -0
- package/apps/storefront/src/lib/content/prose.ts +186 -0
- package/apps/storefront/src/lib/content/richtext.ts +76 -0
- package/apps/storefront/src/lib/content/trustStrip.ts +74 -0
- package/apps/storefront/src/lib/content-edit/client.ts +70 -14
- package/apps/storefront/src/lib/d1/catalog.ts +12 -0
- package/apps/storefront/src/lib/d1/schema-apps.ts +213 -0
- package/apps/storefront/src/lib/dev/apiBase.ts +8 -2
- package/apps/storefront/src/lib/dev/cliSignInCode.ts +65 -115
- package/apps/storefront/src/lib/dev/cockpitStore.ts +65 -0
- package/apps/storefront/src/lib/dev/previewStatus.ts +112 -0
- package/apps/storefront/src/lib/dev/rendezvousBroker.ts +241 -0
- package/apps/storefront/src/lib/dev/subjectReissue.ts +67 -0
- package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +10 -10
- package/apps/storefront/src/lib/env.ts +12 -0
- package/apps/storefront/src/lib/jsonld.ts +12 -17
- package/apps/storefront/src/lib/membership/eligibility.ts +37 -0
- package/apps/storefront/src/lib/monitoring/manifest.ts +302 -0
- package/apps/storefront/src/lib/privacy/emailHint.ts +13 -5
- package/apps/storefront/src/lib/publish/apex-readiness.ts +337 -0
- package/apps/storefront/src/lib/publish/dispatchHealth.ts +269 -0
- package/apps/storefront/src/lib/publish/domainState.ts +351 -0
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +362 -0
- package/apps/storefront/src/lib/rawChrome.ts +34 -3
- package/apps/storefront/src/lib/storyblok/content-model.ts +34 -2
- package/apps/storefront/src/lib/storyblok/provider.ts +11 -4
- package/apps/storefront/src/lib/subscription/model.ts +114 -0
- package/apps/storefront/src/lib/tot/ToTClient.ts +3 -3
- package/apps/storefront/src/lib/tot/query.ts +32 -0
- package/apps/storefront/src/lib/webhooks/cloudflareQueueDispatcher.ts +82 -0
- package/apps/storefront/src/lib/webhooks/deliveryEngine.ts +245 -0
- package/apps/storefront/src/lib/webhooks/deliveryMapper.ts +34 -0
- package/apps/storefront/src/lib/webhooks/deliveryStore.ts +668 -0
- package/apps/storefront/src/lib/webhooks/dispatcher.ts +168 -0
- package/apps/storefront/src/lib/webhooks/emit.ts +167 -0
- package/apps/storefront/src/lib/webhooks/endpointGuard.ts +135 -0
- package/apps/storefront/src/lib/webhooks/events.ts +98 -0
- package/apps/storefront/src/lib/webhooks/getDispatcher.ts +49 -0
- package/apps/storefront/src/lib/webhooks/signing.ts +29 -0
- package/apps/storefront/src/lib/webhooks/webhookSigningKey.ts +146 -0
- package/apps/storefront/src/middleware/index.ts +28 -13
- package/apps/storefront/src/pages/404.astro +21 -9
- package/apps/storefront/src/pages/[...slug].astro +36 -2
- package/apps/storefront/src/pages/admin.astro +29 -3
- package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +60 -0
- package/apps/storefront/src/pages/api/apps/admin/health.ts +44 -0
- package/apps/storefront/src/pages/api/apps/admin/install.ts +100 -0
- package/apps/storefront/src/pages/api/apps/admin/list.ts +26 -0
- package/apps/storefront/src/pages/api/apps/admin/resume.ts +56 -0
- package/apps/storefront/src/pages/api/apps/admin/suspend.ts +57 -0
- package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +74 -0
- package/apps/storefront/src/pages/api/apps/admin/update.ts +103 -0
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +59 -0
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +51 -0
- package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +172 -0
- package/apps/storefront/src/pages/api/apps/oauth/token.ts +87 -0
- package/apps/storefront/src/pages/api/apps/v1/attribution.ts +142 -0
- package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +39 -0
- package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +49 -0
- package/apps/storefront/src/pages/api/apps/v1/health.ts +32 -0
- package/apps/storefront/src/pages/api/apps/v1/inventory.ts +59 -0
- package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +48 -0
- package/apps/storefront/src/pages/api/apps/v1/orders.ts +73 -0
- package/apps/storefront/src/pages/api/apps/v1/reports.ts +21 -0
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +62 -0
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +47 -0
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +48 -1
- package/apps/storefront/src/pages/api/auth/step-up-send.ts +57 -0
- package/apps/storefront/src/pages/api/auth/step-up-verify.ts +129 -0
- package/apps/storefront/src/pages/api/auth/verify.ts +23 -0
- package/apps/storefront/src/pages/api/compliance/preflight.ts +206 -0
- package/apps/storefront/src/pages/api/rum/vitals.ts +54 -0
- package/apps/storefront/src/pages/api/test/dev-session.ts +133 -0
- package/apps/storefront/src/pages/auth/login.astro +197 -41
- package/apps/storefront/src/pages/auth/magic.astro +75 -43
- package/apps/storefront/src/pages/blog/[slug].astro +107 -0
- package/apps/storefront/src/pages/blog/index.astro +98 -0
- package/apps/storefront/src/pages/capabilities.astro +8 -0
- package/apps/storefront/src/pages/cockpit.astro +433 -69
- package/apps/storefront/src/pages/collections/[handle].astro +8 -0
- package/apps/storefront/src/pages/collections/index.astro +10 -2
- package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +119 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +5 -0
- package/apps/storefront/src/pages/index.astro +57 -0
- package/apps/storefront/src/pages/llms.txt.ts +31 -10
- package/apps/storefront/src/pages/products/[handle].astro +100 -9
- package/apps/storefront/src/pages/sitemap.xml.ts +21 -4
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +198 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +7 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +7 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +7 -0
- package/apps/storefront/src/pages/style-guide/index.astro +10 -0
- package/apps/storefront/src/styles/fonts.css +54 -0
- package/apps/storefront/src/styles/global.css +22 -2
- package/apps/storefront/src/themes/schema.ts +3 -25
- package/apps/storefront/tsconfig.json +1 -1
- package/apps/storefront/vitest.config.ts +4 -1
- package/package.json +1 -1
- package/packages/public-runtime/src/candidate-index.ts +311 -0
- package/packages/public-runtime/src/checkout.ts +94 -2
- package/packages/public-runtime/src/compliance/evaluate.ts +265 -0
- package/packages/public-runtime/src/compliance/evidence-signals.ts +81 -0
- package/packages/public-runtime/src/compliance/index.ts +22 -0
- package/packages/public-runtime/src/compliance/pact-report.ts +94 -0
- package/packages/public-runtime/src/compliance/profile.ts +198 -0
- package/packages/public-runtime/src/compliance/ruleset.ts +117 -0
- package/packages/public-runtime/src/compliance/verification.ts +81 -0
- package/packages/public-runtime/src/csp.ts +27 -3
- package/packages/public-runtime/src/customization-reconcile.ts +35 -0
- package/packages/public-runtime/src/customization-runtime.ts +8 -0
- package/packages/public-runtime/src/customization-versioning.ts +17 -0
- package/packages/public-runtime/src/extension-contract.ts +2 -1
- package/packages/public-runtime/src/hash.ts +25 -0
- package/packages/public-runtime/src/index.ts +6 -0
- package/packages/public-runtime/src/membership.ts +353 -0
- package/packages/public-runtime/src/product.ts +24 -2
- package/packages/public-runtime/src/review-trust-proof.ts +194 -0
- package/packages/public-runtime/src/tenant-assets.ts +40 -5
- package/packages/public-runtime/src/tenant.ts +236 -0
- package/packages/public-runtime/src/widget-postmessage.ts +205 -0
- package/scripts/dev/publish.mjs +158 -0
- package/scripts/dev/transient-files.mjs +2 -1
- package/tenants/home/public/fonts/inter-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-800-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-800-normal.woff2 +0 -0
- package/tenants/home/public/pages/storefront.css +23 -0
- package/apps/storefront/src/lib/dev/hostedCockpit.ts +0 -169
|
@@ -34,6 +34,13 @@ query.collection = handle;
|
|
|
34
34
|
if (!query.pageSize) query.pageSize = 9;
|
|
35
35
|
|
|
36
36
|
const { products, page, pageSize, total, facets } = await tot.listProducts(query);
|
|
37
|
+
|
|
38
|
+
// A synthesized-EMPTY collection (zero active members overall — not merely
|
|
39
|
+
// zero under the current facet/price/q filters) still resolves 200 for a direct
|
|
40
|
+
// link, but must not be indexed: it's a crawlable dead end, kept out of the
|
|
41
|
+
// sitemap + directory too. Detect it via the UNFILTERED membership total.
|
|
42
|
+
const { total: memberTotal } = await tot.listProducts({ collection: handle, pageSize: 1 });
|
|
43
|
+
const isEmptyCollection = memberTotal === 0;
|
|
37
44
|
const basePath = `/collections/${handle}`;
|
|
38
45
|
// Tenant-prefixed page path for facet/sort/pagination, which build links by
|
|
39
46
|
// string-concatenating onto this base (so they stay inside a path-routed
|
|
@@ -76,6 +83,7 @@ if (query.priceMin != null || query.priceMax != null) {
|
|
|
76
83
|
title={collection.seo.title || collection.title}
|
|
77
84
|
description={collection.seo.description || (desc ? truncate(desc, 160) : `Shop ${collection.title} at ${tenant.displayName}.`)}
|
|
78
85
|
path={basePath}
|
|
86
|
+
noindex={isEmptyCollection}
|
|
79
87
|
jsonLd={[
|
|
80
88
|
breadcrumbLd(crumbs, canonicalBase),
|
|
81
89
|
itemListLd(products, canonicalBase, (page - 1) * pageSize),
|
|
@@ -8,11 +8,19 @@ import Breadcrumbs from "@/components/Breadcrumbs.astro";
|
|
|
8
8
|
import CollectionCard from "@/components/CollectionCard.astro";
|
|
9
9
|
import EmptyState from "@/components/EmptyState.astro";
|
|
10
10
|
import { breadcrumbLd } from "@/lib/jsonld";
|
|
11
|
+
import { activeMemberCount, nonEmptyCollections } from "@/lib/tot/query";
|
|
11
12
|
|
|
12
13
|
const { tenant, tot, canonicalBase } = Astro.locals;
|
|
13
14
|
// Commerce-only route: marketing tenants have no catalog surface.
|
|
14
15
|
if (!Astro.locals.features.catalog) return new Response(null, { status: 404 });
|
|
15
|
-
|
|
16
|
+
// Directory lists only populated collections — empty synthesized shells are
|
|
17
|
+
// hidden here just as they're kept out of the sitemap/llms.txt. Product counts
|
|
18
|
+
// reflect ACTIVE membership (product_handles can list drafts/dropped items).
|
|
19
|
+
const [allCollections, activeProducts] = await Promise.all([
|
|
20
|
+
tot.listCollections(),
|
|
21
|
+
tot.getAllActiveProducts(),
|
|
22
|
+
]);
|
|
23
|
+
const collections = nonEmptyCollections(allCollections, activeProducts);
|
|
16
24
|
|
|
17
25
|
const crumbs = [
|
|
18
26
|
{ label: tenant.displayName, href: "/" },
|
|
@@ -51,7 +59,7 @@ const crumbs = [
|
|
|
51
59
|
) : (
|
|
52
60
|
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
|
53
61
|
{collections.map((c) => (
|
|
54
|
-
<CollectionCard collection={c} productCount={c
|
|
62
|
+
<CollectionCard collection={c} productCount={activeMemberCount(c, activeProducts)} />
|
|
55
63
|
))}
|
|
56
64
|
</div>
|
|
57
65
|
)}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* /dashboard/<appDomain>/apps — the console's app-health list (PrivateApps
|
|
4
|
+
* epic, D8 Chunk C). Guarded the same way as `../team.astro`: signed-in
|
|
5
|
+
* session + a membership for this appDomain, else bounce.
|
|
6
|
+
*
|
|
7
|
+
* Thin shell over one session-authed endpoint:
|
|
8
|
+
* GET /api/apps/admin/health?tenant=<appDomain> → AppInstallHealthView[]
|
|
9
|
+
* `canManage` is resolved server-side from the SAME `MANAGE_ROLES` set the
|
|
10
|
+
* mutating admin routes gate on, and passed to the client via `data-can-
|
|
11
|
+
* manage` so a `developer` viewer never even sees the suspend/uninstall/
|
|
12
|
+
* rotate buttons rendered (the routes still enforce it independently —
|
|
13
|
+
* this is UX, not the security boundary).
|
|
14
|
+
*/
|
|
15
|
+
export const prerender = false;
|
|
16
|
+
|
|
17
|
+
import { readViewerSession } from "@/lib/auth/route";
|
|
18
|
+
import { membershipFor } from "@/lib/dashboard/tenantSelection";
|
|
19
|
+
import { MANAGE_ROLES } from "@/lib/apps/adminSession.js";
|
|
20
|
+
|
|
21
|
+
const cspNonce = Astro.locals.cspNonce;
|
|
22
|
+
const appDomain = decodeURIComponent(Astro.params.appDomain ?? "");
|
|
23
|
+
|
|
24
|
+
const session = await readViewerSession(Astro);
|
|
25
|
+
if (!session) {
|
|
26
|
+
return Astro.redirect(
|
|
27
|
+
`/auth/login?next=${encodeURIComponent(`/dashboard/${appDomain}/apps`)}`,
|
|
28
|
+
302,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const membership = membershipFor(session.tenants, appDomain, {
|
|
32
|
+
selection: session.staffSelection,
|
|
33
|
+
nowSeconds: Math.floor(Date.now() / 1000),
|
|
34
|
+
});
|
|
35
|
+
if (!membership) {
|
|
36
|
+
return Astro.redirect("/dashboard", 302);
|
|
37
|
+
}
|
|
38
|
+
const canManage = MANAGE_ROLES.has(membership.role);
|
|
39
|
+
const backHref = `/dashboard/${encodeURIComponent(appDomain)}`;
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
<html lang="en">
|
|
43
|
+
<head>
|
|
44
|
+
<meta charset="utf-8" />
|
|
45
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
46
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
47
|
+
<title>Apps — {appDomain}</title>
|
|
48
|
+
<style nonce={cspNonce}>
|
|
49
|
+
:root { color-scheme: light dark; }
|
|
50
|
+
* { box-sizing: border-box; }
|
|
51
|
+
body { font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
52
|
+
margin: 0; min-height: 100vh; padding: 2.5rem 1.25rem; background: #f5f4f0; color: #1c1c1c; }
|
|
53
|
+
@media (prefers-color-scheme: dark) { body { background: #14141a; color: #ececf1; } }
|
|
54
|
+
main { max-width: 48rem; margin-inline: auto; }
|
|
55
|
+
.eyebrow { font: 600 .68rem/1 ui-monospace, monospace; letter-spacing: .12em; text-transform: uppercase; color: #8a7f66; margin: 0 0 .5rem; }
|
|
56
|
+
h1 { font-size: 1.5rem; letter-spacing: -.01em; margin: 0 0 .3rem; }
|
|
57
|
+
.sub { color: #6a6a6a; margin: 0 0 1.5rem; }
|
|
58
|
+
@media (prefers-color-scheme: dark) { .sub { color: #a2a2ad; } }
|
|
59
|
+
#apps { display: flex; flex-direction: column; gap: 1rem; }
|
|
60
|
+
.card { background: #fff; border: 1px solid #e6e4de; border-radius: 12px; padding: 1.15rem 1.25rem; }
|
|
61
|
+
@media (prefers-color-scheme: dark) { .card { background: #1e1e28; border-color: #33333f; } }
|
|
62
|
+
.card .top { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
|
|
63
|
+
.card .name { font-weight: 600; font-size: 1.02rem; }
|
|
64
|
+
.badge { font: 600 .68rem/1 ui-monospace, monospace; text-transform: uppercase; letter-spacing: .06em;
|
|
65
|
+
border: 1px solid currentColor; border-radius: 999px; padding: .2rem .55rem; }
|
|
66
|
+
.badge.status-active, .badge.health-healthy { color: #2e8b45; }
|
|
67
|
+
.badge.status-suspended, .badge.health-degraded { color: #b5860b; }
|
|
68
|
+
.badge.status-uninstalled, .badge.health-unhealthy, .badge.egress-blocked { color: #b13a3a; }
|
|
69
|
+
.badge.egress-ok { color: #2e8b45; }
|
|
70
|
+
.badge.egress-no_endpoint { color: #8a8a8a; }
|
|
71
|
+
dl.meta { display: grid; grid-template-columns: max-content 1fr; gap: .25rem 1rem; margin: .8rem 0 0; font-size: .85rem; }
|
|
72
|
+
dl.meta dt { color: #8a8a8a; }
|
|
73
|
+
dl.meta dd { margin: 0; }
|
|
74
|
+
.traces { font: .78rem/1.4 ui-monospace, monospace; color: #8a8a8a; word-break: break-all; }
|
|
75
|
+
.actions { margin-top: 1rem; display: flex; gap: .5rem; flex-wrap: wrap; }
|
|
76
|
+
.rowbtn { padding: .4rem .8rem; font-size: .82rem; background: transparent; color: inherit;
|
|
77
|
+
border: 1px solid #cfccc4; border-radius: 7px; font-weight: 600; cursor: pointer; }
|
|
78
|
+
@media (prefers-color-scheme: dark) { .rowbtn { border-color: #40404d; } }
|
|
79
|
+
.rowbtn:hover:not(:disabled) { border-color: #b7d0ff; }
|
|
80
|
+
.rowbtn:disabled { opacity: .5; cursor: default; }
|
|
81
|
+
.rowbtn.danger { color: #b13a3a; border-color: #e3b3b3; }
|
|
82
|
+
.msg { margin-top: .8rem; font-size: .85rem; border-radius: 9px; padding: .5rem .7rem; display: none; }
|
|
83
|
+
.msg.ok { display: block; background: #eaf7ee; border: 1px solid #bfe4c9; color: #1b6b34; }
|
|
84
|
+
.msg.err { display: block; background: #fdecec; border: 1px solid #f5c2c2; color: #9a1c1c; }
|
|
85
|
+
@media (prefers-color-scheme: dark) {
|
|
86
|
+
.msg.ok { background: #16311f; border-color: #2f6b41; color: #a7e0ba; }
|
|
87
|
+
.msg.err { background: #3a1f22; border-color: #6b2f34; color: #f6b9b9; }
|
|
88
|
+
}
|
|
89
|
+
.muted { color: #9a9a9a; font-size: .9rem; }
|
|
90
|
+
.who { margin-top: 2rem; font-size: .82rem; color: #8a8a8a; }
|
|
91
|
+
.who a, a.back { color: inherit; }
|
|
92
|
+
.linkbtn { background: none; border: 0; color: inherit; font: inherit; text-decoration: underline; cursor: pointer; padding: 0; margin: 0; }
|
|
93
|
+
.logout { display: inline; }
|
|
94
|
+
</style>
|
|
95
|
+
</head>
|
|
96
|
+
<body>
|
|
97
|
+
<main>
|
|
98
|
+
<p class="eyebrow"><a class="back" href={backHref}>← {appDomain}</a></p>
|
|
99
|
+
<h1>Apps</h1>
|
|
100
|
+
<p class="sub">Installed apps and their webhook health for <strong>{appDomain}</strong>.{!canManage &&
|
|
101
|
+
" Viewing read-only — ask an admin or owner to suspend, uninstall, or rotate credentials."}</p>
|
|
102
|
+
|
|
103
|
+
<div id="apps" data-tenant={appDomain} data-can-manage={String(canManage)}>
|
|
104
|
+
<p class="muted">Loading…</p>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<p class="who">Signed in as {session.email} ·
|
|
108
|
+
<a href="/dashboard">Switch vendor</a> ·
|
|
109
|
+
<form class="logout" method="post" action="/api/auth/logout"><button type="submit" class="linkbtn">Sign out</button></form></p>
|
|
110
|
+
</main>
|
|
111
|
+
|
|
112
|
+
<!--
|
|
113
|
+
Client behavior is a STATIC same-origin file (public/js/dashboard-apps.js),
|
|
114
|
+
referenced via src so the strict CSP allows it through `script-src 'self'` — no
|
|
115
|
+
nonce, no per-build content-hash. Mirrors team.astro's script tag exactly.
|
|
116
|
+
-->
|
|
117
|
+
<script src="/js/dashboard-apps.js" is:inline defer></script>
|
|
118
|
+
</body>
|
|
119
|
+
</html>
|
|
@@ -34,6 +34,7 @@ if (!membership) {
|
|
|
34
34
|
return Astro.redirect("/dashboard", 302);
|
|
35
35
|
}
|
|
36
36
|
const teamHref = `/dashboard/${encodeURIComponent(appDomain)}/team`;
|
|
37
|
+
const appsHref = `/dashboard/${encodeURIComponent(appDomain)}/apps`;
|
|
37
38
|
---
|
|
38
39
|
|
|
39
40
|
<html lang="en">
|
|
@@ -90,6 +91,10 @@ const teamHref = `/dashboard/${encodeURIComponent(appDomain)}/team`;
|
|
|
90
91
|
<p class="h">Preview & deploy →</p>
|
|
91
92
|
<p class="d">The tenant's shared preview: target vs actual, reconcile, preview URL.</p>
|
|
92
93
|
</a>
|
|
94
|
+
<a class="card" href={appsHref}>
|
|
95
|
+
<p class="h">Apps →</p>
|
|
96
|
+
<p class="d">Installed apps, webhook health, and manage suspend/uninstall/rotate.</p>
|
|
97
|
+
</a>
|
|
93
98
|
</div>
|
|
94
99
|
|
|
95
100
|
<p class="who">Signed in as {session.email} ·
|
|
@@ -13,8 +13,12 @@ import ProductCard from "@/components/ProductCard.astro";
|
|
|
13
13
|
import CollectionCard from "@/components/CollectionCard.astro";
|
|
14
14
|
import Img from "@/components/Img.astro";
|
|
15
15
|
import MarketingBlocks from "@/components/marketing/MarketingBlocks.astro";
|
|
16
|
+
import TierCards from "@/components/membership/TierCards.astro";
|
|
16
17
|
import { withBase } from "@/lib/basePath";
|
|
17
18
|
import { rawHtmlPolicy, rawHtmlResponse } from "@/lib/rawMarketingHtml";
|
|
19
|
+
import AppWidgetFrame from "@/components/apps/AppWidgetFrame.astro";
|
|
20
|
+
import { getRegistryService } from "@/lib/apps/adminService.js";
|
|
21
|
+
import { renderWidgetSlot } from "@/lib/apps/widgets/renderSlot.js";
|
|
18
22
|
import type { CatalogCollection, CatalogProduct } from "@tot/public-runtime";
|
|
19
23
|
import {
|
|
20
24
|
isMarketingBlock,
|
|
@@ -22,9 +26,11 @@ import {
|
|
|
22
26
|
type EditorialBlock,
|
|
23
27
|
type NewsletterBlock,
|
|
24
28
|
type PromoTilesBlock,
|
|
29
|
+
type MembershipBandBlock,
|
|
25
30
|
type MarketingBlock,
|
|
26
31
|
type FeaturedProductsBlock,
|
|
27
32
|
} from "@/lib/storyblok/content-model";
|
|
33
|
+
import type { Tier } from "@/lib/membership/model";
|
|
28
34
|
import {
|
|
29
35
|
normalizeProductSectionSource,
|
|
30
36
|
} from "@tot/public-runtime";
|
|
@@ -71,6 +77,7 @@ type Resolved =
|
|
|
71
77
|
| { kind: "promo_tiles"; title?: string; tiles: PromoTilesBlock["tiles"] }
|
|
72
78
|
| { kind: "collections"; title?: string; cols: CatalogCollection[] }
|
|
73
79
|
| { kind: "products"; title?: string; products: CatalogProduct[] }
|
|
80
|
+
| { kind: "membership_band"; block: MembershipBandBlock }
|
|
74
81
|
| { kind: "editorial"; block: EditorialBlock }
|
|
75
82
|
| { kind: "newsletter"; block: NewsletterBlock };
|
|
76
83
|
|
|
@@ -124,6 +131,9 @@ for (const block of marketing ? [] : home.blocks) {
|
|
|
124
131
|
resolved.push({ kind: "products", title: block.title, products });
|
|
125
132
|
break;
|
|
126
133
|
}
|
|
134
|
+
case "membership_band":
|
|
135
|
+
resolved.push({ kind: "membership_band", block });
|
|
136
|
+
break;
|
|
127
137
|
case "editorial":
|
|
128
138
|
resolved.push({ kind: "editorial", block });
|
|
129
139
|
break;
|
|
@@ -132,6 +142,25 @@ for (const block of marketing ? [] : home.blocks) {
|
|
|
132
142
|
break;
|
|
133
143
|
}
|
|
134
144
|
}
|
|
145
|
+
|
|
146
|
+
// Private-app widgets (D5) — home.section slot. Commerce-home only; marketing-
|
|
147
|
+
// tenant homes (raw HTML, above, and the marketing-block home) are out of
|
|
148
|
+
// scope for D5 (deferred). No dark-mode preference infra exists yet — "light"
|
|
149
|
+
// is the only theme hint until that lands.
|
|
150
|
+
const homeSectionWidgets = marketing
|
|
151
|
+
? []
|
|
152
|
+
: await renderWidgetSlot(
|
|
153
|
+
{
|
|
154
|
+
registry: await getRegistryService(),
|
|
155
|
+
tenantId: tenant.tenant_id,
|
|
156
|
+
customizationEnv: Astro.locals.customizationEnv,
|
|
157
|
+
slot: "home.section",
|
|
158
|
+
route: "/",
|
|
159
|
+
locale: tenant.locale,
|
|
160
|
+
theme: "light",
|
|
161
|
+
},
|
|
162
|
+
Astro.locals.activeWidgetPlacements,
|
|
163
|
+
);
|
|
135
164
|
---
|
|
136
165
|
|
|
137
166
|
<Layout
|
|
@@ -165,6 +194,8 @@ for (const block of marketing ? [] : home.blocks) {
|
|
|
165
194
|
<img
|
|
166
195
|
src={t.image}
|
|
167
196
|
alt={t.label}
|
|
197
|
+
width={1600}
|
|
198
|
+
height={900}
|
|
168
199
|
loading="lazy"
|
|
169
200
|
class="absolute inset-0 h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
170
201
|
/>
|
|
@@ -200,6 +231,20 @@ for (const block of marketing ? [] : home.blocks) {
|
|
|
200
231
|
</Section>
|
|
201
232
|
);
|
|
202
233
|
}
|
|
234
|
+
if (r.kind === "membership_band") {
|
|
235
|
+
const b = r.block;
|
|
236
|
+
// Base-path-prefix each tier CTA so home-band links stay in-tenant under
|
|
237
|
+
// path-prefix preview routing (no-op host-routed). TierCards owns the
|
|
238
|
+
// illustrative-figures affordance from the Tier data itself.
|
|
239
|
+
const tiers: Tier[] = b.tiers.map((t) =>
|
|
240
|
+
t.cta ? { ...t, cta: { ...t.cta, href: withBase(basePath, t.cta.href) } } : t,
|
|
241
|
+
);
|
|
242
|
+
return (
|
|
243
|
+
<section class="bg-[var(--color-surface)] py-[clamp(3rem,7vw,6rem)]">
|
|
244
|
+
<TierCards tiers={tiers} eyebrow={b.eyebrow} heading={b.title} subhead={b.subhead} />
|
|
245
|
+
</section>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
203
248
|
if (r.kind === "editorial") {
|
|
204
249
|
const b = r.block;
|
|
205
250
|
const right = b.align !== "left";
|
|
@@ -281,4 +326,16 @@ for (const block of marketing ? [] : home.blocks) {
|
|
|
281
326
|
return null;
|
|
282
327
|
})
|
|
283
328
|
}
|
|
329
|
+
{/* Private-app widgets (D5, home.section) — sandboxed per install. */}
|
|
330
|
+
{homeSectionWidgets.map((w) => (
|
|
331
|
+
<Section>
|
|
332
|
+
<AppWidgetFrame
|
|
333
|
+
endpoint={w.endpoint}
|
|
334
|
+
launchToken={w.launchToken}
|
|
335
|
+
appOrigin={w.appOrigin}
|
|
336
|
+
installId={w.installId}
|
|
337
|
+
title={w.title ?? "App widget"}
|
|
338
|
+
/>
|
|
339
|
+
</Section>
|
|
340
|
+
))}
|
|
284
341
|
</Layout>
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* SEO text; compliance facts come from tenant.compliance.
|
|
11
11
|
*/
|
|
12
12
|
import type { APIContext } from "astro";
|
|
13
|
-
import {
|
|
13
|
+
import { nonEmptyCollections } from "@/lib/tot/query.js";
|
|
14
|
+
import { listBlogPosts } from "@/lib/blog/provider.js";
|
|
14
15
|
|
|
15
16
|
/** Collapse HTML/whitespace to a single trimmed line for a link description. */
|
|
16
17
|
function oneLine(s: string | undefined, max = 140): string | undefined {
|
|
@@ -21,16 +22,23 @@ function oneLine(s: string | undefined, max = 140): string | undefined {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export async function GET(context: APIContext): Promise<Response> {
|
|
24
|
-
const { tot, tenant, canonicalBase } = context.locals;
|
|
25
|
+
const { tot, tenant, canonicalBase, content } = context.locals;
|
|
25
26
|
const base = canonicalBase.replace(/\/$/, "");
|
|
26
|
-
const provider = createContentProvider(tenant);
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
// Use the request-resolved ContentProvider (locals.content) so any published /
|
|
29
|
+
// dev-draft overlay is reflected, matching sitemap.xml.ts (not a fresh provider).
|
|
30
|
+
const [allCollections, activeProducts, pages, featured, home, blogPosts] =
|
|
31
|
+
await Promise.all([
|
|
32
|
+
tot.listCollections(),
|
|
33
|
+
tot.getAllActiveProducts(),
|
|
34
|
+
content.listPages(),
|
|
35
|
+
tot.getFeatured(12),
|
|
36
|
+
content.getHome().catch(() => null),
|
|
37
|
+
listBlogPosts(tenant.tenant_id),
|
|
38
|
+
]);
|
|
39
|
+
// Empty synthesized collections (no active members) never make the guide —
|
|
40
|
+
// an LLM shouldn't be pointed at a URL that resolves to zero products.
|
|
41
|
+
const collections = nonEmptyCollections(allCollections, activeProducts);
|
|
34
42
|
|
|
35
43
|
const lines: string[] = [`# ${tenant.displayName}`, ""];
|
|
36
44
|
|
|
@@ -75,11 +83,24 @@ export async function GET(context: APIContext): Promise<Response> {
|
|
|
75
83
|
lines.push("");
|
|
76
84
|
}
|
|
77
85
|
|
|
86
|
+
if (blogPosts.length) {
|
|
87
|
+
lines.push("## Blog");
|
|
88
|
+
lines.push(`- [All articles](${base}/blog/)`);
|
|
89
|
+
for (const post of blogPosts) {
|
|
90
|
+
const desc = oneLine(post.description || post.excerpt);
|
|
91
|
+
lines.push(`- [${post.title}](${base}/blog/${post.slug}/)${desc ? `: ${desc}` : ""}`);
|
|
92
|
+
}
|
|
93
|
+
lines.push("");
|
|
94
|
+
}
|
|
95
|
+
|
|
78
96
|
lines.push("## Optional");
|
|
79
97
|
lines.push(`- [Full product catalog (XML sitemap)](${base}/sitemap.xml)`);
|
|
80
98
|
lines.push(`- [Search](${base}/search)`);
|
|
81
99
|
lines.push(
|
|
82
|
-
|
|
100
|
+
// Only the always-present graphs are advertised. AggregateRating/Review are
|
|
101
|
+
// emitted per-product ONLY where real review data exists (many stores have
|
|
102
|
+
// none), so we don't claim them site-wide — no fabricated capability.
|
|
103
|
+
"- Every page ships Schema.org JSON-LD (Product, Offer, Breadcrumb, Organization, WebSite, ItemList) for structured extraction; product ratings/reviews appear where the merchant has real review data.",
|
|
83
104
|
);
|
|
84
105
|
lines.push("");
|
|
85
106
|
|
|
@@ -12,24 +12,38 @@ import ProductCard from "@/components/ProductCard.astro";
|
|
|
12
12
|
import Section from "@/components/Section.astro";
|
|
13
13
|
import ImageGallery from "@/components/islands/ImageGallery.tsx";
|
|
14
14
|
import VariantSelector from "@/components/islands/VariantSelector.tsx";
|
|
15
|
+
import CheckoutComplianceGate from "@/components/islands/CheckoutComplianceGate.tsx";
|
|
15
16
|
import RecentlyViewed from "@/components/islands/RecentlyViewed.tsx";
|
|
16
17
|
import NicotineWarning from "@/components/compliance/NicotineWarning.astro";
|
|
17
18
|
import ReviewSummary from "@/components/commerce/ReviewSummary.astro";
|
|
18
19
|
import ReviewList from "@/components/commerce/ReviewList.astro";
|
|
19
20
|
import { isOnSale, isNew, isAvailable, fromPrice } from "@/lib/tot/query";
|
|
20
21
|
import {
|
|
21
|
-
|
|
22
|
+
buildMemberSignedCartPayload,
|
|
22
23
|
normalizePurchaseOptions,
|
|
24
|
+
visiblePurchaseOptions,
|
|
25
|
+
isAutopilotEnabled,
|
|
26
|
+
resolveMembershipRule,
|
|
27
|
+
DEFAULT_STACKING_POLICY,
|
|
28
|
+
readReviewAggregate,
|
|
23
29
|
} from "@tot/public-runtime";
|
|
24
30
|
import type {
|
|
25
31
|
ProductPurchaseOption,
|
|
26
32
|
VariantCartPayload,
|
|
33
|
+
MembershipTierRule,
|
|
34
|
+
MembershipStackingPolicy,
|
|
35
|
+
PurchaseOptionPricingContext,
|
|
27
36
|
} from "@tot/public-runtime";
|
|
28
|
-
import {
|
|
37
|
+
import { resolveCartSigning } from "@/lib/checkoutCommerce";
|
|
38
|
+
import { isRegulatedTenant } from "@/lib/compliance/enforcement";
|
|
39
|
+
import { resolveMemberContext } from "@/lib/membership/eligibility";
|
|
29
40
|
import { truncate } from "@/lib/format";
|
|
30
41
|
import { productLd, breadcrumbLd, reviewLd } from "@/lib/jsonld";
|
|
31
42
|
import { isUsableImageUrl } from "@/lib/placeholder";
|
|
32
43
|
import { withBase } from "@/lib/basePath";
|
|
44
|
+
import AppWidgetFrame from "@/components/apps/AppWidgetFrame.astro";
|
|
45
|
+
import { getRegistryService } from "@/lib/apps/adminService.js";
|
|
46
|
+
import { renderWidgetSlot } from "@/lib/apps/widgets/renderSlot.js";
|
|
33
47
|
|
|
34
48
|
const { handle } = Astro.params;
|
|
35
49
|
const { tenant, tot, canonicalBase, basePath, commerce } = Astro.locals;
|
|
@@ -46,6 +60,23 @@ if (!product || product.status === "archived") {
|
|
|
46
60
|
}
|
|
47
61
|
const isDraft = product.status !== "active";
|
|
48
62
|
|
|
63
|
+
// Private-app widgets (D5) — product.aside slot, one iframe per live install.
|
|
64
|
+
// No dark-mode preference is resolved anywhere in the storefront yet; "light"
|
|
65
|
+
// is the only theme hint until that infra exists.
|
|
66
|
+
const productAsideWidgets = await renderWidgetSlot(
|
|
67
|
+
{
|
|
68
|
+
registry: await getRegistryService(),
|
|
69
|
+
tenantId: tenant.tenant_id,
|
|
70
|
+
customizationEnv: Astro.locals.customizationEnv,
|
|
71
|
+
slot: "product.aside",
|
|
72
|
+
route: `/products/${product.handle}`,
|
|
73
|
+
productHandle: product.handle,
|
|
74
|
+
locale: tenant.locale,
|
|
75
|
+
theme: "light",
|
|
76
|
+
},
|
|
77
|
+
Astro.locals.activeWidgetPlacements,
|
|
78
|
+
);
|
|
79
|
+
|
|
49
80
|
// Related: same collection first, then shared-tag fallback, exclude self+drafts.
|
|
50
81
|
const related = await (async () => {
|
|
51
82
|
const out = new Map<string, typeof product>();
|
|
@@ -79,9 +110,11 @@ function titleCase(s: string) {
|
|
|
79
110
|
const mf = (ns: string, key: string) =>
|
|
80
111
|
product.metafields.find((m) => m.namespace === ns && m.key === key)?.value;
|
|
81
112
|
|
|
82
|
-
// Reviews — REAL
|
|
83
|
-
|
|
84
|
-
|
|
113
|
+
// Reviews — REAL aggregate from the store's reviews app, mirrored to metafields.
|
|
114
|
+
// readReviewAggregate is the single, real-or-absent reader (§F9): it parses both
|
|
115
|
+
// the bare and JSON-blob rating forms and the real count key, and returns {0,0}
|
|
116
|
+
// unless BOTH numbers are real positives — so no fabricated rating can render.
|
|
117
|
+
const { average: reviewAvg, count: reviewCount } = readReviewAggregate(product);
|
|
85
118
|
|
|
86
119
|
// Short description (merchant's concise summary) shown high on the PDP, like the
|
|
87
120
|
// source store. The longer description_html still renders below the buy column.
|
|
@@ -148,6 +181,29 @@ const available = isAvailable(product);
|
|
|
148
181
|
// side so the island can render a signed, tamper-proof form. In `signed` mode a
|
|
149
182
|
// missing secret means we render NO cart (never a tamperable form); the
|
|
150
183
|
// `unsigned-demo` mode needs no secret. Drafts still get a cart (preview links).
|
|
184
|
+
// Membership + Autopilot mechanics (F10). Member pricing renders only once a
|
|
185
|
+
// real eligibility signal flows through resolveMemberContext (Phase 2); Phase 1
|
|
186
|
+
// is anonymous. Autopilot (subscription options) is hidden when the tenant has
|
|
187
|
+
// set commerce.autopilot.enabled = false (hide-at-launch).
|
|
188
|
+
const member = resolveMemberContext();
|
|
189
|
+
const membershipProgram = tenant.commerce?.membership;
|
|
190
|
+
const membershipRule: MembershipTierRule | undefined = resolveMembershipRule(
|
|
191
|
+
member,
|
|
192
|
+
membershipProgram,
|
|
193
|
+
);
|
|
194
|
+
const pricing: {
|
|
195
|
+
membershipRule: MembershipTierRule;
|
|
196
|
+
stacking: MembershipStackingPolicy;
|
|
197
|
+
} | undefined = membershipRule
|
|
198
|
+
? { membershipRule, stacking: membershipProgram?.stacking ?? DEFAULT_STACKING_POLICY }
|
|
199
|
+
: undefined;
|
|
200
|
+
const autopilotEnabled = isAutopilotEnabled(tenant.commerce?.autopilot);
|
|
201
|
+
// Server-side pricing context (F11b). The SIGNED price is the member-adjusted
|
|
202
|
+
// effective price for THIS request's resolved eligibility — never the list price,
|
|
203
|
+
// and never re-derived client-side. Phase 1 resolves anonymous, so this yields the
|
|
204
|
+
// list/subscription price; a Phase-2 member request signs their member price.
|
|
205
|
+
const pricingCtx: PurchaseOptionPricingContext = { member, program: membershipProgram };
|
|
206
|
+
|
|
151
207
|
let checkoutCart:
|
|
152
208
|
| {
|
|
153
209
|
action: string;
|
|
@@ -156,10 +212,16 @@ let checkoutCart:
|
|
|
156
212
|
}
|
|
157
213
|
| undefined;
|
|
158
214
|
if (commerce) {
|
|
159
|
-
|
|
160
|
-
|
|
215
|
+
// Fail closed for production signed checkout: a signed tenant with no
|
|
216
|
+
// cart-validation secret builds NO cart (never a tamperable form). See
|
|
217
|
+
// resolveCartSigning — it also logs the misconfiguration loudly.
|
|
218
|
+
const { enabled, secret } = await resolveCartSigning(commerce);
|
|
219
|
+
if (enabled) {
|
|
161
220
|
try {
|
|
162
|
-
const purchaseOptions =
|
|
221
|
+
const purchaseOptions = visiblePurchaseOptions(
|
|
222
|
+
normalizePurchaseOptions(product),
|
|
223
|
+
{ autopilotEnabled },
|
|
224
|
+
);
|
|
163
225
|
const entries = await Promise.all(
|
|
164
226
|
product.variants.map(
|
|
165
227
|
async (v) => {
|
|
@@ -168,10 +230,12 @@ if (commerce) {
|
|
|
168
230
|
async (purchaseOption) =>
|
|
169
231
|
[
|
|
170
232
|
purchaseOption.id,
|
|
171
|
-
await
|
|
233
|
+
await buildMemberSignedCartPayload(product, v, {
|
|
172
234
|
mode: commerce.mode,
|
|
173
235
|
secret,
|
|
174
236
|
purchaseOption,
|
|
237
|
+
pricing: pricingCtx,
|
|
238
|
+
autopilotEnabled,
|
|
175
239
|
}),
|
|
176
240
|
] as const,
|
|
177
241
|
),
|
|
@@ -298,8 +362,22 @@ const jsonLd = [productJsonLd, breadcrumbLd(crumbs, canonicalBase)];
|
|
|
298
362
|
options={product.options}
|
|
299
363
|
currency={product.price_range.currency}
|
|
300
364
|
locale={tenant.locale}
|
|
365
|
+
productHandle={product.handle}
|
|
366
|
+
productTitle={product.title}
|
|
301
367
|
checkoutCart={checkoutCart}
|
|
368
|
+
pricing={pricing}
|
|
302
369
|
/>
|
|
370
|
+
{/* Regulated-tenant checkout ENFORCEMENT gate (F12/T5). Intercepts the
|
|
371
|
+
managed-cart handoff to clear age + ship-to compliance server-side
|
|
372
|
+
before the external checkout. Only mounts when a store is wired AND
|
|
373
|
+
the tenant is regulated. */}
|
|
374
|
+
{checkoutCart && isRegulatedTenant(tenant) && compliance?.minAge && (
|
|
375
|
+
<CheckoutComplianceGate
|
|
376
|
+
client:load
|
|
377
|
+
preflightUrl={withBase(basePath, "/api/compliance/preflight")}
|
|
378
|
+
minAge={compliance.minAge}
|
|
379
|
+
/>
|
|
380
|
+
)}
|
|
303
381
|
<button
|
|
304
382
|
type="button"
|
|
305
383
|
class="wishlist-toggle group mt-3 inline-flex w-full items-center justify-center gap-2 rounded-[var(--radius-md)] border border-[var(--color-border)] py-2.5 font-display text-sm font-medium transition-colors hover:border-[var(--color-sale)] hover:text-[var(--color-sale)] aria-pressed:border-[var(--color-sale)] aria-pressed:text-[var(--color-sale)]"
|
|
@@ -314,6 +392,19 @@ const jsonLd = [productJsonLd, breadcrumbLd(crumbs, canonicalBase)];
|
|
|
314
392
|
</button>
|
|
315
393
|
</div>
|
|
316
394
|
|
|
395
|
+
{/* Private-app widgets (D5, product.aside) — sandboxed per install. */}
|
|
396
|
+
{productAsideWidgets.map((w) => (
|
|
397
|
+
<div class="mt-7">
|
|
398
|
+
<AppWidgetFrame
|
|
399
|
+
endpoint={w.endpoint}
|
|
400
|
+
launchToken={w.launchToken}
|
|
401
|
+
appOrigin={w.appOrigin}
|
|
402
|
+
installId={w.installId}
|
|
403
|
+
title={w.title ?? "App widget"}
|
|
404
|
+
/>
|
|
405
|
+
</div>
|
|
406
|
+
))}
|
|
407
|
+
|
|
317
408
|
{/* Rich description + attribute lines — matches the source store's PDP:
|
|
318
409
|
the longer description, then plain "Label: value" lines (no spec table). */}
|
|
319
410
|
<div class="mt-9 border-t border-[var(--color-border)] pt-7">
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-tenant XML sitemap. Built from the resolved tenant's canonical base +
|
|
3
3
|
* catalog. ACTIVE products only — getAllActiveProducts() excludes the draft
|
|
4
|
-
* canary (`trail-cap`), so drafts never leak into the sitemap.
|
|
5
|
-
*
|
|
4
|
+
* canary (`trail-cap`), so drafts never leak into the sitemap. Collections are
|
|
5
|
+
* filtered to those with at least one active product (nonEmptyCollections), so
|
|
6
|
+
* empty synthesized collection shells never become crawlable dead ends.
|
|
7
|
+
* Editorial slugs come from the ContentProvider. The canonical base is resolved
|
|
8
|
+
* per-request (prod domain OR path-prefixed preview host), so every emitted
|
|
9
|
+
* loc is prefix-correct without special-casing here.
|
|
6
10
|
*/
|
|
7
11
|
import type { APIContext } from "astro";
|
|
12
|
+
import { nonEmptyCollections } from "@/lib/tot/query.js";
|
|
13
|
+
import { listBlogPosts } from "@/lib/blog/provider.js";
|
|
8
14
|
|
|
9
15
|
interface SitemapUrl {
|
|
10
16
|
loc: string;
|
|
@@ -22,7 +28,7 @@ function escapeXml(value: string): string {
|
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
export async function GET(context: APIContext): Promise<Response> {
|
|
25
|
-
const { tot, canonicalBase, content, features } = context.locals;
|
|
31
|
+
const { tot, tenant, canonicalBase, content, features } = context.locals;
|
|
26
32
|
const base = canonicalBase.replace(/\/$/, "");
|
|
27
33
|
|
|
28
34
|
const urls: SitemapUrl[] = [{ loc: `${base}/` }];
|
|
@@ -30,10 +36,13 @@ export async function GET(context: APIContext): Promise<Response> {
|
|
|
30
36
|
// Commerce URLs only for tenants with a catalog sitemap. Marketing tenants
|
|
31
37
|
// emit homepage + content pages, keeping product/collection routes out.
|
|
32
38
|
if (features.commerceSitemap) {
|
|
33
|
-
const [products,
|
|
39
|
+
const [products, allCollections] = await Promise.all([
|
|
34
40
|
tot.getAllActiveProducts(),
|
|
35
41
|
tot.listCollections(),
|
|
36
42
|
]);
|
|
43
|
+
// Only collections with at least one active product — empty synthesized
|
|
44
|
+
// shells (all-draft/dropped membership) stay out of the sitemap.
|
|
45
|
+
const collections = nonEmptyCollections(allCollections, products);
|
|
37
46
|
urls.push({ loc: `${base}/collections` });
|
|
38
47
|
for (const c of collections) {
|
|
39
48
|
urls.push({ loc: `${base}/collections/${c.handle}` });
|
|
@@ -56,6 +65,14 @@ export async function GET(context: APIContext): Promise<Response> {
|
|
|
56
65
|
urls.push({ loc: `${base}/${page.slug}` });
|
|
57
66
|
}
|
|
58
67
|
|
|
68
|
+
const blogPosts = await listBlogPosts(tenant.tenant_id);
|
|
69
|
+
if (blogPosts.length > 0) {
|
|
70
|
+
urls.push({ loc: `${base}/blog/` });
|
|
71
|
+
for (const post of blogPosts) {
|
|
72
|
+
urls.push({ loc: `${base}/blog/${post.slug}/`, lastmod: post.modifiedAt || post.publishedAt });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
59
76
|
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
60
77
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
61
78
|
${urls
|