@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
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ship operations workspace (unit dg7) — the PURE composer behind the admin
|
|
3
|
+
* Publish tab's operations surface that sits ALONGSIDE the go-live core
|
|
4
|
+
* (AdminPublishTab.astro's change queue + accept/reject + dispatch status).
|
|
5
|
+
*
|
|
6
|
+
* dg7 is DELTA-scoped: the go-live session already delivered the Publish-tab
|
|
7
|
+
* core (the change queue from GET /api/changes, Go-live/Reject, the live-version
|
|
8
|
+
* + dispatch-status panel, and the owner-only Domain section). This module adds
|
|
9
|
+
* the operations workspace BEYOND that core — the three read/act surfaces woven
|
|
10
|
+
* into the same tab:
|
|
11
|
+
*
|
|
12
|
+
* 1. DIFF VIEW — for one pending candidate, the artifact-level diff of what
|
|
13
|
+
* changed (its rendered immutable version vs the current live version),
|
|
14
|
+
* tied to the exact tenant Gitea head SHA, with the u5 immutable
|
|
15
|
+
* `/preview/<tenant>/rev/<sha>/` link and the dg3 evidence AUTHORITY
|
|
16
|
+
* (authoritative / stale / missing) — never a diff a reviewer would mistake
|
|
17
|
+
* for the head when a last-good version actually renders.
|
|
18
|
+
* 2. VERSIONS — the tenant's published-version / promotion history
|
|
19
|
+
* (`listPromotions`, u2/u3 promotion_status), each row carrying the tenant
|
|
20
|
+
* Gitea SHA (`gitea-only-tenant-content-authority`), its promotion time, a
|
|
21
|
+
* `/rev/<sha>/` link, and — marking the current live one.
|
|
22
|
+
* 3. ROLLBACK — pick a prior version and roll back THROUGH the one orchestrator
|
|
23
|
+
* (`/api/changes/rollback` → `rollbackCandidateAtomic` / u8's
|
|
24
|
+
* `staticRollbackOrchestrator`), rendering only the HONEST terminal state
|
|
25
|
+
* (`rolled_back` when VERIFIED; `publish_pending` in flight; `publish_failed`
|
|
26
|
+
* / `blocked` fail-closed) — never a false "rolled back".
|
|
27
|
+
*
|
|
28
|
+
* COMPOSES, never rebuilds:
|
|
29
|
+
* - the u5 immutable link is `reviewDashboardRevisionHref` verbatim;
|
|
30
|
+
* - the dg3 authority is `candidateEvidenceAuthority` /
|
|
31
|
+
* `classifyCandidateForRender` verbatim;
|
|
32
|
+
* - the versions come from the SAME `KVVersionStore.listPromotions` the
|
|
33
|
+
* "one step back" rollback resolution reads;
|
|
34
|
+
* - the rollback ACTION is the existing `/api/changes/rollback` seam — this
|
|
35
|
+
* module only INTERPRETS its honest response, it never re-decides a ship.
|
|
36
|
+
*
|
|
37
|
+
* PURE (no I/O): the endpoints inject already-read KV data, so every honest-state
|
|
38
|
+
* decision is unit-tested without a Worker/KV — mirroring the reviewDashboard.ts /
|
|
39
|
+
* candidatePreviewState.ts split.
|
|
40
|
+
*
|
|
41
|
+
* HONEST BOUNDARY (surfaced, never faked): a FULLY-STATIC tenant serves its public
|
|
42
|
+
* site from an external `versions/<digest>/` archive whose per-version treeDigests
|
|
43
|
+
* live only in S3 — enumerable by the Node-only `listRollbackTargets`
|
|
44
|
+
* (`scripts/publish/lib/static-rollback.mjs`), NOT by the Worker. So the console
|
|
45
|
+
* can list a static tenant's promotion history + `/rev` links + the CURRENT live
|
|
46
|
+
* digest, but it cannot resolve an ARBITRARY prior version's archive digest, which
|
|
47
|
+
* `/api/changes/rollback` fail-closed requires (`targetDigest`). `rollbackMode`
|
|
48
|
+
* names this: a `control-plane` tenant rolls back one-click (targetVersionId only);
|
|
49
|
+
* a `static-archive` tenant's per-version rollback is honestly deferred to the ship
|
|
50
|
+
* CLI / CI that enumerates the archive — never a one-click that would 400.
|
|
51
|
+
*/
|
|
52
|
+
import { reviewDashboardRevisionHref } from "@/lib/preview/reviewDashboard";
|
|
53
|
+
import {
|
|
54
|
+
candidateEvidenceAuthority,
|
|
55
|
+
classifyCandidateForRender,
|
|
56
|
+
defaultBlockedReason,
|
|
57
|
+
type CandidateEvidenceAuthority,
|
|
58
|
+
} from "@/lib/preview/candidatePreviewState";
|
|
59
|
+
import type { ReviewEnvironment, PromotionEvent } from "@tot/public-runtime";
|
|
60
|
+
|
|
61
|
+
// ── Versions / promotion history ───────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/** One published version in the tenant's promotion history (u2/u3 promotion_status). */
|
|
64
|
+
export interface WorkspaceVersion {
|
|
65
|
+
/** The tenant Gitea SHA the version serves — the content identity
|
|
66
|
+
* (`gitea-only-tenant-content-authority`). */
|
|
67
|
+
versionId: string;
|
|
68
|
+
/** How the version was published (a Gitea commit, or a content hash). */
|
|
69
|
+
source: "commit" | "hash" | null;
|
|
70
|
+
/** ISO time the version was PROMOTED onto `live` (the promotion event). */
|
|
71
|
+
promotedAt: string;
|
|
72
|
+
/** ISO time the version was first published (from its site-version manifest). */
|
|
73
|
+
createdAt: string | null;
|
|
74
|
+
/** Artifact count in the version manifest, or `null` when the manifest is absent. */
|
|
75
|
+
artifactCount: number | null;
|
|
76
|
+
/**
|
|
77
|
+
* The content-addressed `treeDigest`, when the console can honestly know it —
|
|
78
|
+
* i.e. ONLY the current live version of a static tenant (read back from
|
|
79
|
+
* `live/manifest.json`). `null` for every other row: a prior static version's
|
|
80
|
+
* digest lives only in the S3 archive the Worker can't enumerate. Never fabricated.
|
|
81
|
+
*/
|
|
82
|
+
digest: string | null;
|
|
83
|
+
/** The u5 immutable `/preview/<tenant>/rev/<sha>/` deep-link, or `null` without a SHA. */
|
|
84
|
+
revHref: string | null;
|
|
85
|
+
/** The version currently serving live (the one you never roll back TO). */
|
|
86
|
+
isCurrent: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** How a rollback is honestly driven for this tenant (see the module HONEST BOUNDARY note). */
|
|
90
|
+
export type RollbackMode = "control-plane" | "static-archive";
|
|
91
|
+
|
|
92
|
+
/** The versions-panel view model. */
|
|
93
|
+
export interface VersionsView {
|
|
94
|
+
/** The current live version + (static) the digest `live/` actually serves. */
|
|
95
|
+
current: { versionId: string | null; digest: string | null };
|
|
96
|
+
/** Promotion history, newest first, current marked. */
|
|
97
|
+
versions: WorkspaceVersion[];
|
|
98
|
+
/** Whether the tenant publishes its public site to an external static target. */
|
|
99
|
+
staticPublish: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* `control-plane` → a prior version rolls back one-click via `targetVersionId`.
|
|
102
|
+
* `static-archive` → per-version rollback is deferred to the ship CLI / CI that
|
|
103
|
+
* enumerates the `versions/` archive (the Worker can't resolve the archive digest).
|
|
104
|
+
*/
|
|
105
|
+
rollbackMode: RollbackMode;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** A single promoted version enriched with the manifest facts the panel shows. */
|
|
109
|
+
export interface VersionManifestFacts {
|
|
110
|
+
source: "commit" | "hash" | null;
|
|
111
|
+
createdAt: string | null;
|
|
112
|
+
artifactCount: number | null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface BuildVersionsViewInput {
|
|
116
|
+
tenantId: string;
|
|
117
|
+
/** Live-promotion history, newest first (`KVVersionStore.listPromotions`). */
|
|
118
|
+
promotions: PromotionEvent[];
|
|
119
|
+
/** The current live version id (the live-channel pointer), or `null`. */
|
|
120
|
+
currentVersionId: string | null;
|
|
121
|
+
/** The digest `live/` serves — static tenants only; `null` otherwise. */
|
|
122
|
+
currentDigest: string | null;
|
|
123
|
+
/** Per-version manifest facts, keyed by versionId (absent ⇒ unknown facts). */
|
|
124
|
+
manifests: Map<string, VersionManifestFacts>;
|
|
125
|
+
staticPublish: boolean;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Assemble the versions panel from the promotion history + per-version manifest
|
|
130
|
+
* facts + the current-live pointer. Pure. The current live row is marked; the
|
|
131
|
+
* digest is populated ONLY for the current live of a static tenant (never faked
|
|
132
|
+
* for priors). `revHref` is the u5 immutable link.
|
|
133
|
+
*/
|
|
134
|
+
export function buildVersionsView(input: BuildVersionsViewInput): VersionsView {
|
|
135
|
+
const { tenantId, promotions, currentVersionId, currentDigest, manifests, staticPublish } = input;
|
|
136
|
+
|
|
137
|
+
const versions: WorkspaceVersion[] = promotions.map((p) => {
|
|
138
|
+
const facts = manifests.get(p.versionId);
|
|
139
|
+
const isCurrent = currentVersionId != null && p.versionId === currentVersionId;
|
|
140
|
+
return {
|
|
141
|
+
versionId: p.versionId,
|
|
142
|
+
source: facts?.source ?? null,
|
|
143
|
+
promotedAt: p.at,
|
|
144
|
+
createdAt: facts?.createdAt ?? null,
|
|
145
|
+
artifactCount: facts?.artifactCount ?? null,
|
|
146
|
+
// Honest: a digest is known only for the current live of a static tenant.
|
|
147
|
+
digest: isCurrent && staticPublish ? currentDigest : null,
|
|
148
|
+
revHref: reviewDashboardRevisionHref(tenantId, p.versionId),
|
|
149
|
+
isCurrent,
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
current: { versionId: currentVersionId, digest: staticPublish ? currentDigest : null },
|
|
155
|
+
versions,
|
|
156
|
+
staticPublish,
|
|
157
|
+
rollbackMode: staticPublish ? "static-archive" : "control-plane",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Diff view ──────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
export type DiffChangeKind = "added" | "removed" | "changed";
|
|
164
|
+
|
|
165
|
+
/** One artifact path that differs between the candidate and the current live version. */
|
|
166
|
+
export interface DiffEntry {
|
|
167
|
+
path: string;
|
|
168
|
+
change: DiffChangeKind;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The diff-view model for one pending candidate. */
|
|
172
|
+
export interface CandidateDiffView {
|
|
173
|
+
changeId: string;
|
|
174
|
+
/** The candidate's exact head SHA — the reconcile TARGET (tenant Gitea sha). */
|
|
175
|
+
headSha: string | null;
|
|
176
|
+
/** The immutable version actually rendered + diffed — the reconcile ACTUAL. */
|
|
177
|
+
versionId: string | null;
|
|
178
|
+
/** The u5 immutable link to the exact reviewed SHA, or `null` without a head SHA. */
|
|
179
|
+
revHref: string | null;
|
|
180
|
+
/** dg3 evidence authority: authoritative (tied to head) / stale (last-good) / missing. */
|
|
181
|
+
authority: CandidateEvidenceAuthority;
|
|
182
|
+
/** The artifact-level diff (candidate rendered version vs current live), sorted by path. */
|
|
183
|
+
files: DiffEntry[];
|
|
184
|
+
/** True when the candidate has a renderable version to diff (dg3 `ready`). */
|
|
185
|
+
renderable: boolean;
|
|
186
|
+
/** When NOT renderable, the honest reason (names why) — never a fake empty diff. */
|
|
187
|
+
blockedReason: string | null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface BuildCandidateDiffInput {
|
|
191
|
+
tenantId: string;
|
|
192
|
+
env: ReviewEnvironment;
|
|
193
|
+
/** The candidate rendered version's artifacts (path → contentHash), or `null`. */
|
|
194
|
+
candidateArtifacts: Record<string, string> | null;
|
|
195
|
+
/** The current live version's artifacts (path → contentHash), or `null` (nothing live yet). */
|
|
196
|
+
liveArtifacts: Record<string, string> | null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Diff a candidate's rendered version against the current live version, at the
|
|
201
|
+
* artifact level (each path carries a content hash in its site-version manifest).
|
|
202
|
+
* Composes dg3's `classifyCandidateForRender` (only a `ready` candidate has a
|
|
203
|
+
* version to diff — a blocked/queued one yields an HONEST `blockedReason`, never a
|
|
204
|
+
* fabricated empty diff) and `candidateEvidenceAuthority` (so a diff of a last-good
|
|
205
|
+
* version that LAGS the head is flagged `stale`, not passed off as the head).
|
|
206
|
+
* Pure.
|
|
207
|
+
*/
|
|
208
|
+
export function buildCandidateDiff(input: BuildCandidateDiffInput): CandidateDiffView {
|
|
209
|
+
const { tenantId, env, candidateArtifacts, liveArtifacts } = input;
|
|
210
|
+
const classification = classifyCandidateForRender(env);
|
|
211
|
+
const authority = candidateEvidenceAuthority(env);
|
|
212
|
+
const revHref = reviewDashboardRevisionHref(tenantId, env.headSha);
|
|
213
|
+
|
|
214
|
+
if (classification.kind !== "ready") {
|
|
215
|
+
const reason =
|
|
216
|
+
classification.kind === "blocked"
|
|
217
|
+
? classification.reason
|
|
218
|
+
: defaultBlockedReason(env.status);
|
|
219
|
+
return {
|
|
220
|
+
changeId: env.changeId,
|
|
221
|
+
headSha: env.headSha,
|
|
222
|
+
versionId: null,
|
|
223
|
+
revHref,
|
|
224
|
+
authority,
|
|
225
|
+
files: [],
|
|
226
|
+
renderable: false,
|
|
227
|
+
blockedReason: reason,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const files = diffArtifacts(candidateArtifacts ?? {}, liveArtifacts ?? {});
|
|
232
|
+
return {
|
|
233
|
+
changeId: env.changeId,
|
|
234
|
+
headSha: env.headSha,
|
|
235
|
+
versionId: classification.versionId,
|
|
236
|
+
revHref,
|
|
237
|
+
authority,
|
|
238
|
+
files,
|
|
239
|
+
renderable: true,
|
|
240
|
+
blockedReason: null,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Artifact-level diff: paths added / removed / content-changed, sorted by path. */
|
|
245
|
+
export function diffArtifacts(
|
|
246
|
+
candidate: Record<string, string>,
|
|
247
|
+
live: Record<string, string>,
|
|
248
|
+
): DiffEntry[] {
|
|
249
|
+
const entries: DiffEntry[] = [];
|
|
250
|
+
for (const [path, hash] of Object.entries(candidate)) {
|
|
251
|
+
if (!(path in live)) entries.push({ path, change: "added" });
|
|
252
|
+
else if (live[path] !== hash) entries.push({ path, change: "changed" });
|
|
253
|
+
}
|
|
254
|
+
for (const path of Object.keys(live)) {
|
|
255
|
+
if (!(path in candidate)) entries.push({ path, change: "removed" });
|
|
256
|
+
}
|
|
257
|
+
return entries.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Rollback response interpretation ─────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
/** The normalized honest terminal state of a rollback, for the surface to render. */
|
|
263
|
+
export type RollbackState =
|
|
264
|
+
| "rolled_back" // VERIFIED live on the prior version — the ONLY "rolled back" claim
|
|
265
|
+
| "publish_pending" // restored + dispatched, not yet verified — NOT "rolled back"
|
|
266
|
+
| "publish_failed" // fail-closed: unwired / dispatch failed / verification failed
|
|
267
|
+
| "blocked" // a gate refused (capability / no prior / race) — nothing moved
|
|
268
|
+
| "error"; // transport / unexpected shape
|
|
269
|
+
|
|
270
|
+
export interface RollbackOutcome {
|
|
271
|
+
state: RollbackState;
|
|
272
|
+
/** True ONLY for `rolled_back` (the target is verified to serve the prior version). */
|
|
273
|
+
rolledBack: boolean;
|
|
274
|
+
/** The version restored TO, when known. */
|
|
275
|
+
toVersionId: string | null;
|
|
276
|
+
/** A human-legible message for the surface. */
|
|
277
|
+
message: string;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Interpret the `/api/changes/rollback` response into ONE honest terminal state,
|
|
282
|
+
* for BOTH planes without the surface having to know which it hit:
|
|
283
|
+
* - a HOSTED (control-plane) rollback returns `{ rolledBack: true, toVersionId }`
|
|
284
|
+
* on success (the pointer flip IS the rollback) or `{ error, reason }` on refusal;
|
|
285
|
+
* - a STATIC rollback returns the orchestrator's honest `{ state, ... }` (u8).
|
|
286
|
+
* The rule the surface depends on: `rolledBack` is true ONLY for a verified
|
|
287
|
+
* `rolled_back` — a `publish_pending` is NEVER rendered as rolled back
|
|
288
|
+
* (`storefront-atomic-accept-is-ship-orchestrator` / `static-publish-fails-closed`).
|
|
289
|
+
* Pure.
|
|
290
|
+
*/
|
|
291
|
+
export function interpretRollbackResponse(
|
|
292
|
+
httpOk: boolean,
|
|
293
|
+
body: {
|
|
294
|
+
state?: unknown;
|
|
295
|
+
rolledBack?: unknown;
|
|
296
|
+
toVersionId?: unknown;
|
|
297
|
+
message?: unknown;
|
|
298
|
+
error?: unknown;
|
|
299
|
+
reason?: unknown;
|
|
300
|
+
},
|
|
301
|
+
): RollbackOutcome {
|
|
302
|
+
const toVersionId = typeof body.toVersionId === "string" ? body.toVersionId : null;
|
|
303
|
+
|
|
304
|
+
// Static plane: the orchestrator's explicit honest state wins.
|
|
305
|
+
if (typeof body.state === "string") {
|
|
306
|
+
const state = normalizeRollbackState(body.state);
|
|
307
|
+
const message =
|
|
308
|
+
typeof body.message === "string" && body.message
|
|
309
|
+
? body.message
|
|
310
|
+
: typeof body.error === "string" && body.error
|
|
311
|
+
? body.error
|
|
312
|
+
: rollbackStateFallbackMessage(state);
|
|
313
|
+
return { state, rolledBack: state === "rolled_back", toVersionId, message };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Control-plane: a bare success flag, or a typed refusal.
|
|
317
|
+
if (httpOk && body.rolledBack === true) {
|
|
318
|
+
return {
|
|
319
|
+
state: "rolled_back",
|
|
320
|
+
rolledBack: true,
|
|
321
|
+
toVersionId,
|
|
322
|
+
message: toVersionId ? `Rolled back live to ${toVersionId}.` : "Rolled back live.",
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const reason = typeof body.reason === "string" ? body.reason : null;
|
|
327
|
+
const message =
|
|
328
|
+
typeof body.error === "string" && body.error
|
|
329
|
+
? body.error + (reason ? ` (${reason})` : "")
|
|
330
|
+
: "Rollback failed.";
|
|
331
|
+
// A control-plane refusal is a gate refusal (capability / no prior / conflict) —
|
|
332
|
+
// nothing moved; treat as blocked (never a false rolled-back).
|
|
333
|
+
return { state: "blocked", rolledBack: false, toVersionId, message };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function normalizeRollbackState(raw: string): RollbackState {
|
|
337
|
+
switch (raw) {
|
|
338
|
+
case "rolled_back":
|
|
339
|
+
case "publish_pending":
|
|
340
|
+
case "publish_failed":
|
|
341
|
+
case "blocked":
|
|
342
|
+
return raw;
|
|
343
|
+
default:
|
|
344
|
+
return "error";
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function rollbackStateFallbackMessage(state: RollbackState): string {
|
|
349
|
+
switch (state) {
|
|
350
|
+
case "rolled_back":
|
|
351
|
+
return "Rolled back live.";
|
|
352
|
+
case "publish_pending":
|
|
353
|
+
return "Restored the pointer and dispatched the re-publish — not yet verified live. Refresh to re-check.";
|
|
354
|
+
case "publish_failed":
|
|
355
|
+
return "Rollback failed — the publish path is unwired or verification failed. Nothing false went live.";
|
|
356
|
+
case "blocked":
|
|
357
|
+
return "Rollback was refused by a gate — nothing changed.";
|
|
358
|
+
case "error":
|
|
359
|
+
default:
|
|
360
|
+
return "Rollback returned an unexpected response.";
|
|
361
|
+
}
|
|
362
|
+
}
|
|
@@ -99,7 +99,9 @@ function renderBrand(brand: ChromeBrand, opts: { footer?: boolean } = {}): strin
|
|
|
99
99
|
const inner = validLogo
|
|
100
100
|
? `<img src="${esc(logo.src)}" alt="${esc(logo.alt)}"${
|
|
101
101
|
logo.width ? ` width="${logo.width}"` : ""
|
|
102
|
-
}${logo.height ? ` height="${logo.height}"` : ""}
|
|
102
|
+
}${logo.height ? ` height="${logo.height}"` : ""} loading="${
|
|
103
|
+
opts.footer ? "lazy" : "eager"
|
|
104
|
+
}" decoding="async">`
|
|
103
105
|
: brand.wordmark?.length
|
|
104
106
|
? renderWordmark(brand.wordmark)
|
|
105
107
|
: esc(brand.label);
|
|
@@ -279,7 +281,7 @@ function renderFooter(footer: ChromeFooter): string {
|
|
|
279
281
|
const logo = brand?.logo && /^(https?:|\/)/.test(brand.logo.src)
|
|
280
282
|
? `<img src="${esc(brand.logo.src)}" alt="${esc(brand.logo.alt)}"${
|
|
281
283
|
brand.logo.width ? ` width="${brand.logo.width}"` : ""
|
|
282
|
-
}${brand.logo.height ? ` height="${brand.logo.height}"` : ""}>`
|
|
284
|
+
}${brand.logo.height ? ` height="${brand.logo.height}"` : ""} loading="lazy" decoding="async">`
|
|
283
285
|
: brand?.wordmark?.length
|
|
284
286
|
? `<span class="brand">${renderWordmark(brand.wordmark)}</span>`
|
|
285
287
|
: "";
|
|
@@ -445,6 +447,31 @@ export const CHROME_BEHAVIOR_JS = `(function () {
|
|
|
445
447
|
/** The platform-shipped chrome stylesheet every rendered wrapper links (unit F2). */
|
|
446
448
|
export const SHARED_CHROME_STYLESHEET = "/shared/commerce-chrome.css";
|
|
447
449
|
|
|
450
|
+
/**
|
|
451
|
+
* The platform-shipped RAW-PAGE-BODY marketing stylesheet (unit F5). Linked only
|
|
452
|
+
* when a tenant opts in (chrome.sharedMarketingCss) and AFTER the chrome sheet, so
|
|
453
|
+
* its page-level variants (e.g. .sec-head) win over chrome's home-section ones.
|
|
454
|
+
*/
|
|
455
|
+
export const SHARED_MARKETING_STYLESHEET = "/shared/commerce-marketing.css";
|
|
456
|
+
|
|
457
|
+
/** MIME type for a favicon href, derived from its extension (SVG marks are common now). */
|
|
458
|
+
function faviconMime(href: string): string {
|
|
459
|
+
const ext = href.split(/[?#]/)[0]?.split(".").pop()?.toLowerCase();
|
|
460
|
+
switch (ext) {
|
|
461
|
+
case "svg":
|
|
462
|
+
return "image/svg+xml";
|
|
463
|
+
case "ico":
|
|
464
|
+
return "image/x-icon";
|
|
465
|
+
case "jpg":
|
|
466
|
+
case "jpeg":
|
|
467
|
+
return "image/jpeg";
|
|
468
|
+
case "webp":
|
|
469
|
+
return "image/webp";
|
|
470
|
+
default:
|
|
471
|
+
return "image/png";
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
448
475
|
/**
|
|
449
476
|
* Render the canonical chrome wrapper document for a tenant, from its config.
|
|
450
477
|
*
|
|
@@ -459,9 +486,13 @@ export function renderRawChrome(chrome: TenantChrome): string {
|
|
|
459
486
|
const head = chrome.head ?? {};
|
|
460
487
|
const headLinks = [
|
|
461
488
|
head.themeColor ? `<meta name="theme-color" content="${esc(head.themeColor)}">` : "",
|
|
462
|
-
head.favicon
|
|
489
|
+
head.favicon
|
|
490
|
+
? `<link rel="icon" type="${faviconMime(head.favicon)}" href="${esc(head.favicon)}">`
|
|
491
|
+
: "",
|
|
463
492
|
...(head.themeStylesheets ?? []).map((h) => `<link rel="stylesheet" href="${esc(h)}">`),
|
|
464
493
|
`<link rel="stylesheet" href="${SHARED_CHROME_STYLESHEET}">`,
|
|
494
|
+
// Opt-in raw-page-body layer, after the chrome sheet so its variants win (F5).
|
|
495
|
+
chrome.sharedMarketingCss ? `<link rel="stylesheet" href="${SHARED_MARKETING_STYLESHEET}">` : "",
|
|
465
496
|
]
|
|
466
497
|
.filter(Boolean)
|
|
467
498
|
.join("\n");
|
|
@@ -9,12 +9,15 @@
|
|
|
9
9
|
* parent/child + Shared Components plan.
|
|
10
10
|
*/
|
|
11
11
|
import type { ProductSectionSource } from "@tot/public-runtime";
|
|
12
|
+
import type { ProseSection } from "@/lib/content/prose";
|
|
13
|
+
import type { Tier } from "@/lib/membership/model";
|
|
12
14
|
|
|
13
15
|
export type HomeBlock =
|
|
14
16
|
| HeroBlock
|
|
15
17
|
| PromoTilesBlock
|
|
16
18
|
| FeaturedCollectionsBlock
|
|
17
19
|
| FeaturedProductsBlock
|
|
20
|
+
| MembershipBandBlock
|
|
18
21
|
| EditorialBlock
|
|
19
22
|
| NewsletterBlock;
|
|
20
23
|
|
|
@@ -59,6 +62,22 @@ export interface FeaturedProductsBlock {
|
|
|
59
62
|
limit?: number;
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Membership "band" on the commerce home — the Guest / Member / Autopilot tier
|
|
67
|
+
* comparison, driven by the platform <TierCards> (F4) widget from tenant data.
|
|
68
|
+
* The widget owns the illustrative-figures affordance (a tier is an EXAMPLE
|
|
69
|
+
* unless it asserts `real: true`), so a tenant surfaces its membership model on
|
|
70
|
+
* the home page without copying tier-card HTML. The block carries the same
|
|
71
|
+
* {@link Tier} shape the standalone membership page uses.
|
|
72
|
+
*/
|
|
73
|
+
export interface MembershipBandBlock {
|
|
74
|
+
component: "membership_band";
|
|
75
|
+
eyebrow?: string;
|
|
76
|
+
title?: string;
|
|
77
|
+
subhead?: string;
|
|
78
|
+
tiers: Tier[];
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
export interface EditorialBlock {
|
|
63
82
|
component: "editorial";
|
|
64
83
|
eyebrow?: string;
|
|
@@ -249,11 +268,24 @@ export interface EditorialPage {
|
|
|
249
268
|
title: string;
|
|
250
269
|
seoTitle?: string;
|
|
251
270
|
seoDescription?: string;
|
|
252
|
-
/**
|
|
253
|
-
|
|
271
|
+
/**
|
|
272
|
+
* Sanitized HTML body (legacy / raw path). Optional: a page authored with
|
|
273
|
+
* structured {@link ProseSection}s omits it and drives the body from `sections`
|
|
274
|
+
* instead (spec §F4). The renderer prefers `sections` when present.
|
|
275
|
+
*/
|
|
276
|
+
body_html?: string;
|
|
277
|
+
/**
|
|
278
|
+
* Structured policy/prose body (spec §F4) — headings, paragraphs, lists,
|
|
279
|
+
* callouts, a contact block, needs-review flags. When present, the page route
|
|
280
|
+
* renders <ProseSections> from this instead of `set:html(body_html)`, so a
|
|
281
|
+
* tenant drives a full policy page from JSON with no HTML/CSS to copy.
|
|
282
|
+
*/
|
|
283
|
+
sections?: ProseSection[];
|
|
254
284
|
/** Optional hero eyebrow/intro. */
|
|
255
285
|
eyebrow?: string;
|
|
256
286
|
intro?: string;
|
|
287
|
+
/** ISO date (YYYY-MM-DD) rendered as a "Last updated" line (structured pages). */
|
|
288
|
+
updated?: string;
|
|
257
289
|
/** When true, listed in the footer policy column. */
|
|
258
290
|
isPolicy?: boolean;
|
|
259
291
|
}
|
|
@@ -353,18 +353,25 @@ function mapHome(content: Record<string, any>): HomeContent {
|
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
function mapPage(content: Record<string, any>, slug: string): EditorialPage {
|
|
356
|
-
//
|
|
357
|
-
//
|
|
356
|
+
// A page is either structured (F4 `sections`) OR raw HTML (`body_html`/`body`).
|
|
357
|
+
// Structured pages drive the body from `sections`; otherwise we expect a
|
|
358
|
+
// rendered-HTML field (a raw richtext object would need a resolver — we throw
|
|
359
|
+
// so the local fallback serves it instead).
|
|
360
|
+
const sections = Array.isArray(content.sections) ? content.sections : undefined;
|
|
358
361
|
const body_html = content.body_html ?? content.body;
|
|
359
|
-
if (typeof body_html !== "string")
|
|
362
|
+
if (!sections && typeof body_html !== "string") {
|
|
363
|
+
throw new Error(`storyblok page ${slug}: no sections and body not HTML`);
|
|
364
|
+
}
|
|
360
365
|
return {
|
|
361
366
|
slug: slug.replace(/^pages\//, ""),
|
|
362
367
|
title: content.title ?? slug,
|
|
363
368
|
seoTitle: content.seoTitle ?? content.seo_title,
|
|
364
369
|
seoDescription: content.seoDescription ?? content.seo_description,
|
|
365
|
-
body_html,
|
|
370
|
+
body_html: typeof body_html === "string" ? body_html : undefined,
|
|
371
|
+
sections,
|
|
366
372
|
eyebrow: content.eyebrow,
|
|
367
373
|
intro: content.intro,
|
|
374
|
+
updated: content.updated,
|
|
368
375
|
isPolicy: !!content.isPolicy,
|
|
369
376
|
};
|
|
370
377
|
}
|
|
@@ -43,6 +43,74 @@ export interface SubscriptionFigure {
|
|
|
43
43
|
real?: boolean;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The account-management actions a subscriber can take on an Autopilot order
|
|
48
|
+
* (F10). The storefront never EXECUTES these — each is a handoff into the
|
|
49
|
+
* tenant's product-owned manage-subscription portal (see {@link
|
|
50
|
+
* SubscriptionManagePortal}); this enum just gives the entry points stable,
|
|
51
|
+
* typed identities so the copy, ordering, and analytics hooks are consistent.
|
|
52
|
+
*/
|
|
53
|
+
export type SubscriptionManageAction =
|
|
54
|
+
| "skip"
|
|
55
|
+
| "pause"
|
|
56
|
+
| "resume"
|
|
57
|
+
| "cancel"
|
|
58
|
+
| "reschedule"
|
|
59
|
+
| "quantity"
|
|
60
|
+
| "swap"
|
|
61
|
+
| "address";
|
|
62
|
+
|
|
63
|
+
/** Stable order for listing the manage actions on a surface. */
|
|
64
|
+
export const SUBSCRIPTION_MANAGE_ACTIONS: SubscriptionManageAction[] = [
|
|
65
|
+
"skip",
|
|
66
|
+
"pause",
|
|
67
|
+
"resume",
|
|
68
|
+
"cancel",
|
|
69
|
+
"reschedule",
|
|
70
|
+
"quantity",
|
|
71
|
+
"swap",
|
|
72
|
+
"address",
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
/** Default label + description copy per manage action. Display only. */
|
|
76
|
+
export const MANAGE_ACTION_META: Record<
|
|
77
|
+
SubscriptionManageAction,
|
|
78
|
+
{ label: string; description: string }
|
|
79
|
+
> = {
|
|
80
|
+
skip: {
|
|
81
|
+
label: "Skip a delivery",
|
|
82
|
+
description: "Skip your next shipment and pick back up after.",
|
|
83
|
+
},
|
|
84
|
+
pause: {
|
|
85
|
+
label: "Pause your subscription",
|
|
86
|
+
description: "Take a break — pause billing and shipments, resume anytime.",
|
|
87
|
+
},
|
|
88
|
+
resume: {
|
|
89
|
+
label: "Resume your subscription",
|
|
90
|
+
description: "Start deliveries again from a paused subscription.",
|
|
91
|
+
},
|
|
92
|
+
cancel: {
|
|
93
|
+
label: "Cancel anytime",
|
|
94
|
+
description: "Cancel your subscription with no penalty.",
|
|
95
|
+
},
|
|
96
|
+
reschedule: {
|
|
97
|
+
label: "Reschedule delivery",
|
|
98
|
+
description: "Move your next delivery date earlier or later.",
|
|
99
|
+
},
|
|
100
|
+
quantity: {
|
|
101
|
+
label: "Change quantity",
|
|
102
|
+
description: "Update how much ships each cycle.",
|
|
103
|
+
},
|
|
104
|
+
swap: {
|
|
105
|
+
label: "Swap products",
|
|
106
|
+
description: "Change which products are in your subscription.",
|
|
107
|
+
},
|
|
108
|
+
address: {
|
|
109
|
+
label: "Update address or payment",
|
|
110
|
+
description: "Change where it ships or how you pay.",
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
46
114
|
/**
|
|
47
115
|
* One manage-subscription entry point (e.g. "Update delivery date", "Pause or
|
|
48
116
|
* cancel"). This is an EDUCATION/HANDOFF link only — it points at the
|
|
@@ -56,6 +124,52 @@ export interface SubscriptionManageEntry {
|
|
|
56
124
|
description?: string;
|
|
57
125
|
/** Handoff URL into the tenant's product-owned manage-subscription flow. */
|
|
58
126
|
href: string;
|
|
127
|
+
/** The typed action this entry hands off to (analytics/audit hook). */
|
|
128
|
+
action?: SubscriptionManageAction;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The provider-handoff CONTRACT for subscription management. The storefront owns
|
|
133
|
+
* only the entry points; the tenant's managed-checkout provider (Phase 2) owns
|
|
134
|
+
* the actual skip/pause/cancel/reschedule/quantity execution behind `baseHref`.
|
|
135
|
+
* A tenant supplies its portal URL and (optionally) the subset of actions it
|
|
136
|
+
* offers; `hrefForAction` overrides the default deep-link shape when the provider
|
|
137
|
+
* uses path- rather than query-based deep links.
|
|
138
|
+
*/
|
|
139
|
+
export interface SubscriptionManagePortal {
|
|
140
|
+
/** Base URL of the tenant's manage-subscription portal. */
|
|
141
|
+
baseHref: string;
|
|
142
|
+
/** Actions offered (in listing order). Defaults to every action. */
|
|
143
|
+
actions?: SubscriptionManageAction[];
|
|
144
|
+
/** Deep-link builder; defaults to `?action=<action>` on `baseHref`. */
|
|
145
|
+
hrefForAction?: (action: SubscriptionManageAction, baseHref: string) => string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Default deep link into the manage portal for an action. */
|
|
149
|
+
function defaultActionHref(action: SubscriptionManageAction, baseHref: string): string {
|
|
150
|
+
return `${baseHref}${baseHref.includes("?") ? "&" : "?"}action=${action}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Build the manage-subscription entry points for a tenant's portal — the F10
|
|
155
|
+
* skip / pause / cancel / reschedule / quantity (+ resume / swap / address)
|
|
156
|
+
* handoffs. Each entry is a plain link into `portal.baseHref`; the storefront
|
|
157
|
+
* hands off, the provider executes.
|
|
158
|
+
*/
|
|
159
|
+
export function buildManageEntries(
|
|
160
|
+
portal: SubscriptionManagePortal,
|
|
161
|
+
): SubscriptionManageEntry[] {
|
|
162
|
+
const actions = portal.actions ?? SUBSCRIPTION_MANAGE_ACTIONS;
|
|
163
|
+
const hrefFor = portal.hrefForAction ?? defaultActionHref;
|
|
164
|
+
return actions.map((action) => {
|
|
165
|
+
const meta = MANAGE_ACTION_META[action];
|
|
166
|
+
return {
|
|
167
|
+
label: meta.label,
|
|
168
|
+
description: meta.description,
|
|
169
|
+
href: hrefFor(action, portal.baseHref),
|
|
170
|
+
action,
|
|
171
|
+
};
|
|
172
|
+
});
|
|
59
173
|
}
|
|
60
174
|
|
|
61
175
|
/**
|
|
@@ -51,9 +51,9 @@ export class FixtureToTClient implements ToTClient {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
private scoped(): CatalogProduct[] {
|
|
54
|
-
// Fixtures are
|
|
55
|
-
//
|
|
56
|
-
return this.products.filter((p) => p.tenant_id === this.scope
|
|
54
|
+
// Fixtures are now multi-tenant: every read is scoped to this client's
|
|
55
|
+
// tenant so one tenant can never see another's products.
|
|
56
|
+
return this.products.filter((p) => p.tenant_id === this.scope);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
async listProducts(query: ListProductsQuery = {}): Promise<ListProductsResponse> {
|