@tokenoftrust/storefront-runner 1.4.0 → 1.4.2-rc.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.
Files changed (193) hide show
  1. package/apps/storefront/astro.config.mjs +17 -1
  2. package/apps/storefront/integrations/materialize-guard.mjs +56 -0
  3. package/apps/storefront/migrations-apps/0001_woozy_lyja.sql +22 -0
  4. package/apps/storefront/migrations-apps/0002_good_switch.sql +26 -0
  5. package/apps/storefront/migrations-apps/0003_mute_marauders.sql +12 -0
  6. package/apps/storefront/migrations-apps/meta/0001_snapshot.json +990 -0
  7. package/apps/storefront/migrations-apps/meta/0002_snapshot.json +1160 -0
  8. package/apps/storefront/migrations-apps/meta/0003_snapshot.json +1235 -0
  9. package/apps/storefront/migrations-apps/meta/_journal.json +21 -0
  10. package/apps/storefront/package.json +0 -1
  11. package/apps/storefront/public/js/dashboard-team.js +38 -2
  12. package/apps/storefront/src/components/Seo.astro +65 -1
  13. package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +878 -0
  14. package/apps/storefront/src/components/admin/AdminBlogTab.astro +1432 -0
  15. package/apps/storefront/src/components/admin/AdminCustomersTab.astro +3 -7
  16. package/apps/storefront/src/components/admin/AdminPublishTab.astro +1756 -1382
  17. package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +1 -3
  18. package/apps/storefront/src/components/blog/BlogPaywall.astro +87 -0
  19. package/apps/storefront/src/components/blog/PostAccessBadge.astro +43 -0
  20. package/apps/storefront/src/components/blog/PostGrid.astro +57 -0
  21. package/apps/storefront/src/components/blog/ReactionBar.astro +214 -0
  22. package/apps/storefront/src/layouts/Layout.astro +48 -3
  23. package/apps/storefront/src/lib/activity/alerts.ts +426 -0
  24. package/apps/storefront/src/lib/activity/changeActorAttribution.ts +85 -0
  25. package/apps/storefront/src/lib/activity/deployVersion.ts +120 -0
  26. package/apps/storefront/src/lib/activity/ingest.ts +188 -0
  27. package/apps/storefront/src/lib/activity/ingestAuth.ts +105 -0
  28. package/apps/storefront/src/lib/activity/killSwitch.ts +80 -0
  29. package/apps/storefront/src/lib/activity/query.ts +403 -0
  30. package/apps/storefront/src/lib/activity/recordActivity.ts +101 -0
  31. package/apps/storefront/src/lib/activity/store.ts +120 -0
  32. package/apps/storefront/src/lib/activity/uiActor.ts +150 -0
  33. package/apps/storefront/src/lib/activity/workerCommit.ts +70 -0
  34. package/apps/storefront/src/lib/admin/adminShell.ts +290 -0
  35. package/apps/storefront/src/lib/admin/ai/approvalMode.ts +2 -3
  36. package/apps/storefront/src/lib/admin/ai/audit.ts +3 -5
  37. package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +7 -7
  38. package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +3 -3
  39. package/apps/storefront/src/lib/admin/orderClient.ts +3 -5
  40. package/apps/storefront/src/lib/admin/services/orders.ts +17 -19
  41. package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +4 -6
  42. package/apps/storefront/src/lib/admin/viewmodels/settings.ts +1 -1
  43. package/apps/storefront/src/lib/apps/adminService.ts +4 -4
  44. package/apps/storefront/src/lib/apps/adminSession.ts +53 -44
  45. package/apps/storefront/src/lib/apps/apiAuth.ts +2 -2
  46. package/apps/storefront/src/lib/apps/apiRoute.ts +2 -2
  47. package/apps/storefront/src/lib/apps/catalogMapper.ts +1 -1
  48. package/apps/storefront/src/lib/apps/credentials.ts +2 -2
  49. package/apps/storefront/src/lib/apps/gatewayKeys.ts +4 -4
  50. package/apps/storefront/src/lib/apps/healthAggregate.ts +5 -5
  51. package/apps/storefront/src/lib/apps/orders/attributionService.ts +2 -2
  52. package/apps/storefront/src/lib/apps/orders/customerHash.ts +1 -1
  53. package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +12 -12
  54. package/apps/storefront/src/lib/apps/orders/idempotency.ts +7 -7
  55. package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +1 -1
  56. package/apps/storefront/src/lib/apps/orders/orderMapper.ts +2 -2
  57. package/apps/storefront/src/lib/apps/orders/ordersStore.ts +6 -6
  58. package/apps/storefront/src/lib/apps/registryService.ts +6 -6
  59. package/apps/storefront/src/lib/apps/scopes.ts +2 -2
  60. package/apps/storefront/src/lib/apps/tokenIssuer.ts +4 -4
  61. package/apps/storefront/src/lib/apps/tokenVerifier.ts +6 -6
  62. package/apps/storefront/src/lib/apps/widgets/eligibility.ts +2 -2
  63. package/apps/storefront/src/lib/apps/widgets/frameProps.ts +3 -3
  64. package/apps/storefront/src/lib/apps/widgets/launchToken.ts +4 -4
  65. package/apps/storefront/src/lib/apps/widgets/placements.ts +2 -2
  66. package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +1 -1
  67. package/apps/storefront/src/lib/auth/adminEntry.ts +9 -9
  68. package/apps/storefront/src/lib/auth/identityToken.ts +6 -14
  69. package/apps/storefront/src/lib/auth/loginGate.ts +75 -13
  70. package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +6 -4
  71. package/apps/storefront/src/lib/auth/operatorSession.ts +113 -0
  72. package/apps/storefront/src/lib/auth/route.ts +44 -4
  73. package/apps/storefront/src/lib/auth/session.ts +7 -8
  74. package/apps/storefront/src/lib/auth/totAccessClient.ts +8 -18
  75. package/apps/storefront/src/lib/basePath.ts +9 -2
  76. package/apps/storefront/src/lib/blog/access.ts +29 -0
  77. package/apps/storefront/src/lib/blog/collection.ts +313 -0
  78. package/apps/storefront/src/lib/blog/markdown.ts +174 -0
  79. package/apps/storefront/src/lib/blog/provider.ts +5 -0
  80. package/apps/storefront/src/lib/blog/reactions.ts +87 -0
  81. package/apps/storefront/src/lib/blog/rss.ts +92 -0
  82. package/apps/storefront/src/lib/blog/teaser.ts +83 -0
  83. package/apps/storefront/src/lib/blog/types.ts +34 -0
  84. package/apps/storefront/src/lib/blog/viewCounts.ts +243 -0
  85. package/apps/storefront/src/lib/d1/catalog.ts +2 -2
  86. package/apps/storefront/src/lib/d1/schema-apps.ts +147 -14
  87. package/apps/storefront/src/lib/dev/activityIngestToken.ts +2 -4
  88. package/apps/storefront/src/lib/dev/activityStore.ts +3 -5
  89. package/apps/storefront/src/lib/dev/apiBase.ts +1 -4
  90. package/apps/storefront/src/lib/dev/cockpitStore.ts +1 -9
  91. package/apps/storefront/src/lib/dev/devDraft.ts +1 -1
  92. package/apps/storefront/src/lib/dev/previewStatus.ts +9 -21
  93. package/apps/storefront/src/lib/dev/rendezvousBroker.ts +9 -24
  94. package/apps/storefront/src/lib/edgeCache.ts +17 -0
  95. package/apps/storefront/src/lib/env.ts +51 -0
  96. package/apps/storefront/src/lib/http/fetchTransport.ts +63 -0
  97. package/apps/storefront/src/lib/i18n.ts +25 -0
  98. package/apps/storefront/src/lib/jsonld.ts +118 -0
  99. package/apps/storefront/src/lib/monitoring/manifest.ts +11 -11
  100. package/apps/storefront/src/lib/newsletter/confirmationEmail.ts +30 -0
  101. package/apps/storefront/src/lib/newsletter/consent.ts +23 -0
  102. package/apps/storefront/src/lib/newsletter/digest.ts +117 -0
  103. package/apps/storefront/src/lib/newsletter/resendClient.ts +79 -0
  104. package/apps/storefront/src/lib/newsletter/send.ts +265 -0
  105. package/apps/storefront/src/lib/newsletter/subscribers.ts +156 -0
  106. package/apps/storefront/src/lib/publish/adminPublishTab.ts +3519 -0
  107. package/apps/storefront/src/lib/publish/apex-readiness.ts +41 -18
  108. package/apps/storefront/src/lib/publish/dispatchHealth.ts +5 -7
  109. package/apps/storefront/src/lib/publish/domainState.ts +22 -13
  110. package/apps/storefront/src/lib/publish/panelPolling.ts +78 -0
  111. package/apps/storefront/src/lib/publish/shipWorkspace.ts +100 -42
  112. package/apps/storefront/src/lib/publish/stageBundleUpload.ts +215 -0
  113. package/apps/storefront/src/lib/rate/kvWindow.ts +53 -0
  114. package/apps/storefront/src/lib/search/index.ts +2 -2
  115. package/apps/storefront/src/lib/seo/alternates.ts +42 -0
  116. package/apps/storefront/src/lib/seo/meta.ts +65 -0
  117. package/apps/storefront/src/lib/social/golive.ts +212 -0
  118. package/apps/storefront/src/lib/social/notify.ts +32 -0
  119. package/apps/storefront/src/lib/social/tokenStore.ts +171 -0
  120. package/apps/storefront/src/lib/the-build/provider.ts +27 -0
  121. package/apps/storefront/src/lib/the-build/readingTime.ts +35 -0
  122. package/apps/storefront/src/lib/the-build/toc.ts +95 -0
  123. package/apps/storefront/src/lib/tot/ToTClient.ts +1 -1
  124. package/apps/storefront/src/lib/tot/d1Client.ts +2 -2
  125. package/apps/storefront/src/lib/tot/totClientInterface.ts +3 -4
  126. package/apps/storefront/src/lib/webhooks/deliveryStore.ts +16 -17
  127. package/apps/storefront/src/middleware/index.ts +130 -2
  128. package/apps/storefront/src/pages/admin/ops-timeline.astro +420 -0
  129. package/apps/storefront/src/pages/admin.astro +52 -1206
  130. package/apps/storefront/src/pages/api/activity.ts +137 -0
  131. package/apps/storefront/src/pages/api/admin/activity-alerts.ts +73 -0
  132. package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +27 -1
  133. package/apps/storefront/src/pages/api/apps/admin/install.ts +26 -1
  134. package/apps/storefront/src/pages/api/apps/admin/resume.ts +24 -1
  135. package/apps/storefront/src/pages/api/apps/admin/suspend.ts +24 -1
  136. package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +25 -1
  137. package/apps/storefront/src/pages/api/apps/admin/update.ts +24 -1
  138. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +11 -0
  139. package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +11 -0
  140. package/apps/storefront/src/pages/api/apps/v1/attribution.ts +20 -0
  141. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +12 -0
  142. package/apps/storefront/src/pages/api/blog/[collection]/[slug]/react.ts +104 -0
  143. package/apps/storefront/src/pages/api/cache-purge.ts +11 -0
  144. package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +30 -0
  145. package/apps/storefront/src/pages/api/newsletter/[collection]/subscribe.ts +146 -0
  146. package/apps/storefront/src/pages/api/newsletter/confirm.ts +75 -0
  147. package/apps/storefront/src/pages/api/newsletter/unsubscribe.ts +90 -0
  148. package/apps/storefront/src/pages/api/publish/stage-bundle.ts +70 -0
  149. package/apps/storefront/src/pages/api/rum/vitals.ts +21 -2
  150. package/apps/storefront/src/pages/auth/login.astro +5 -4
  151. package/apps/storefront/src/pages/blog/[slug].astro +96 -16
  152. package/apps/storefront/src/pages/blog/author/[author].astro +140 -0
  153. package/apps/storefront/src/pages/blog/category/[category].astro +67 -0
  154. package/apps/storefront/src/pages/blog/index.astro +2 -0
  155. package/apps/storefront/src/pages/blog/tag/[tag].astro +65 -0
  156. package/apps/storefront/src/pages/cockpit.astro +74 -12
  157. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +15 -11
  158. package/apps/storefront/src/pages/search.astro +1 -1
  159. package/apps/storefront/src/pages/sitemap.xml.ts +20 -0
  160. package/apps/storefront/src/pages/the-build/[slug].astro +129 -0
  161. package/apps/storefront/src/pages/the-build/index.astro +146 -0
  162. package/apps/storefront/src/pages/the-build/rss.xml.ts +33 -0
  163. package/apps/storefront/src/styles/admin.css +1080 -0
  164. package/apps/storefront/tsconfig.json +5 -0
  165. package/package.json +1 -1
  166. package/packages/public-runtime/package.json +1 -0
  167. package/packages/public-runtime/src/activity/README.md +146 -0
  168. package/packages/public-runtime/src/activity/catalog.ts +553 -0
  169. package/packages/public-runtime/src/activity/event.ts +168 -0
  170. package/packages/public-runtime/src/activity/index.ts +16 -0
  171. package/packages/public-runtime/src/activity/redaction.ts +263 -0
  172. package/packages/public-runtime/src/candidate-index.ts +13 -0
  173. package/packages/public-runtime/src/catalog-d1.ts +1 -1
  174. package/packages/public-runtime/src/customization-preview.ts +25 -2
  175. package/packages/public-runtime/src/customization-runtime.ts +125 -3
  176. package/packages/public-runtime/src/customization-versioning.ts +10 -0
  177. package/packages/public-runtime/src/index.ts +7 -0
  178. package/packages/public-runtime/src/static-bundle.ts +357 -0
  179. package/packages/public-runtime/src/tenant.ts +36 -8
  180. package/scripts/build/copy-tenant-assets.mjs +14 -4
  181. package/scripts/dev/ai-edit.mjs +1 -1
  182. package/scripts/dev/checkout-watch.mjs +2 -2
  183. package/scripts/dev/file-browser.mjs +2 -4
  184. package/scripts/dev/file-writer.mjs +1 -1
  185. package/scripts/dev/git-status.mjs +2 -2
  186. package/scripts/dev/locate-handler.mjs +3 -3
  187. package/scripts/dev/port-check.mjs +3 -3
  188. package/scripts/dev/publish.mjs +5 -9
  189. package/scripts/dev/shot.mjs +1 -1
  190. package/scripts/dev/unified-diff.mjs +8 -3
  191. package/scripts/tot-dev.mjs +6 -9
  192. package/apps/storefront/src/lib/webhooks/signing.ts +0 -29
  193. package/apps/storefront/src/lib/webhooks/webhookSigningKey.ts +0 -146
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Machine-verifiable apex-cutover READINESS gate (unit u9) — the green
2
+ * Machine-verifiable apex-cutover READINESS gate — the green
3
3
  * preconditions that MUST all pass before an owner apex (DNS) cutover is
4
- * permitted. This is the DELTA on top of the go-live session's apex machinery:
4
+ * permitted. It consumes the go-live apex machinery:
5
5
  * go-live owns the admin Domain Connect/Rollback button, the DNS cutover/rollback
6
6
  * CI (`www-domain.yml` → `cutover-dns.mjs`, prior-records persisted to
7
- * `s3://.../meta/`), and the HMAC completion callback; u9 CONSUMES those and
7
+ * `s3://.../meta/`), and the HMAC completion callback; this gate CONSUMES those and
8
8
  * GATES them. Nothing here mutates DNS — it decides whether a cutover is ALLOWED.
9
9
  *
10
10
  * The gate is a PURE function over a signals bundle so the exact policy is
@@ -12,7 +12,7 @@
12
12
  * `change/ship`'s CLI and the `acceptGate`/`apexCutoverGate` pure decisions). The
13
13
  * server endpoint (`GET /api/domain/readiness`) assembles the signals from the
14
14
  * live sources via {@link assembleApexReadinessSignals} — the owner capability
15
- * (u10 `resolveShipCapabilities`), the per-tenant `DomainState` (in-flight run +
15
+ * (`resolveShipCapabilities`), the per-tenant `DomainState` (in-flight run +
16
16
  * the CAPTURED revert target), and the consumed dual-run parity / target-health /
17
17
  * live-digest evidence record — and the admin Domain display + the `tot go-live`
18
18
  * CLI both read the SAME endpoint, so the enforcement gate and the surfaces never
@@ -20,24 +20,24 @@
20
20
  *
21
21
  * EVIDENCE CONTRACT (the seam the go-live dual-run/health CI feeds): the parity,
22
22
  * target-health and live-digest signals are CONSUMED from a per-tenant evidence
23
- * record persisted at {@link readinessEvidenceKey} (KV). u9 does not RE-RUN
23
+ * record persisted at {@link readinessEvidenceKey} (KV). The gate does not RE-RUN
24
24
  * dual-run — it reads the record `scripts/publish/dual-run.mjs` + the health
25
25
  * probe produce. An absent/garbage record normalises fail-closed (no parity, no
26
26
  * digest, unhealthy), so readiness BLOCKS until the go-live CI writes real
27
27
  * evidence — never a false "ready" from a missing feed.
28
28
  *
29
- * FAIL-CLOSED throughout (decision `static-publish-fails-closed-reviewed-sha`):
29
+ * FAIL-CLOSED throughout:
30
30
  * a missing, stale, or failed signal BLOCKS the cutover with an honest,
31
31
  * itemized reason — never a false "ready". Absence denies. The gate is
32
32
  * ADVISORY-vs-authoritative honest: it reports readiness, but the actual cutover
33
- * is still owner-gated at `apexCutoverGate`/`guardApexCutover` (u10) and executed
33
+ * is still owner-gated at `apexCutoverGate`/`guardApexCutover` and executed
34
34
  * only by the go-live CI — a green readiness verdict never bypasses those.
35
35
  */
36
36
 
37
37
  /**
38
38
  * The consumed dual-run parity evidence (produced by `scripts/publish/dual-run.mjs`
39
- * — the T7 CloudFront-vs-baseline structural diff the pending-dns-cutover gate
40
- * wants). u9 CONSUMES this shape; it does not re-run dual-run. Green = the hard
39
+ * — the CloudFront-vs-baseline structural diff the pending-dns-cutover gate
40
+ * wants). This shape is consumed here; dual-run is not re-run. Green = the hard
41
41
  * gate dual-run itself enforces: zero CloudFront route failures. `statusMismatches`
42
42
  * are surfaced for review (content legitimately differs during a migration) but do
43
43
  * NOT block, exactly as `dual-run.mjs` treats them. `digest` ties the evidence to
@@ -73,7 +73,7 @@ export interface RevertTarget {
73
73
  export interface ApexReadinessSignals {
74
74
  /**
75
75
  * The acting principal holds the OWNER-ONLY apex-cutover capability
76
- * (`resolveShipCapabilities(principal).apexCutover`, u10). A `ship-on-behalf`
76
+ * (`resolveShipCapabilities(principal).apexCutover`). A `ship-on-behalf`
77
77
  * developer or an unauthenticated caller is false → denied.
78
78
  */
79
79
  ownerCapable: boolean;
@@ -96,8 +96,8 @@ export interface ApexReadinessSignals {
96
96
  targetHealthy: boolean;
97
97
  /**
98
98
  * The digest currently published live (the reviewed tree the cutover would
99
- * point traffic at). Evidence must be tied to THIS digest to count as fresh
100
- * (`static-publish-fails-closed-reviewed-sha`). Null ⇒ freshness fails closed.
99
+ * point traffic at). Evidence must be tied to THIS digest to count as fresh.
100
+ * Null ⇒ freshness fails closed.
101
101
  */
102
102
  liveDigest: string | null;
103
103
  /** The captured revert target — a rollback must have somewhere to restore FROM. */
@@ -122,6 +122,14 @@ export interface ApexReadinessCheck {
122
122
  ok: boolean;
123
123
  /** Why it passed or, more importantly, why it's blocking — always actionable. */
124
124
  detail: string;
125
+ /**
126
+ * The concrete action that clears this precondition when it's blocking — the
127
+ * "how to get ready" guide, carried on the gate itself so the admin panel and
128
+ * `tot go-live` surface identical remediation (never a divergent side doc). Only
129
+ * meaningful/shown when `ok` is false; the full operator walkthrough (secret
130
+ * provisioning, IAM) lives in `docs/runbooks/apex-cutover-readiness.md`.
131
+ */
132
+ remedy: string;
125
133
  }
126
134
 
127
135
  /** The full readiness verdict: ready iff EVERY check passed; itemized either way. */
@@ -146,7 +154,7 @@ export interface ApexReadiness {
146
154
  export function computeApexReadiness(signals: ApexReadinessSignals): ApexReadiness {
147
155
  const checks: ApexReadinessCheck[] = [];
148
156
 
149
- // 1. Owner-only capability (u10). A non-owner is denied outright — this is the
157
+ // 1. Owner-only capability. A non-owner is denied outright — this is the
150
158
  // same floor `apexCutoverGate` enforces, surfaced as a readiness item.
151
159
  checks.push({
152
160
  id: "owner-capability",
@@ -155,6 +163,7 @@ export function computeApexReadiness(signals: ApexReadinessSignals): ApexReadine
155
163
  detail: signals.ownerCapable
156
164
  ? "you hold the owner-only apex-cutover capability"
157
165
  : "apex cutover is owner-only — a ship-on-behalf developer cannot cut over the apex",
166
+ remedy: "Sign in as the store owner — apex cutover cannot be delegated to a ship-on-behalf developer.",
158
167
  });
159
168
 
160
169
  // 2. No run in flight — two concurrent DNS mutations is the race DomainState guards.
@@ -165,6 +174,7 @@ export function computeApexReadiness(signals: ApexReadinessSignals): ApexReadine
165
174
  detail: signals.runInFlight
166
175
  ? "a domain connect/rollback run is already in flight — wait for it to complete"
167
176
  : "no domain run is in flight",
177
+ remedy: "Wait for the in-flight connect/rollback run to finish (watch the run log), then re-check.",
168
178
  });
169
179
 
170
180
  // 3. Dual-run parity — zero CloudFront route failures (the hard gate dual-run.mjs
@@ -186,6 +196,10 @@ export function computeApexReadiness(signals: ApexReadinessSignals): ApexReadine
186
196
  (dr.statusMismatches > 0
187
197
  ? ` (${dr.statusMismatches} status mismatch(es) surfaced for review — non-blocking)`
188
198
  : ""),
199
+ remedy:
200
+ "Publish tokenoftrust.com (a www-publish run) so the dual-run records fresh evidence. " +
201
+ "First time only: provision the WWW_READINESS_EVIDENCE_URL variable + WWW_READINESS_EVIDENCE_SECRET " +
202
+ "secret (see the apex-cutover-readiness runbook).",
189
203
  });
190
204
 
191
205
  // 4. Target health — the apex target (CloudFront/static origin) is serving.
@@ -196,11 +210,14 @@ export function computeApexReadiness(signals: ApexReadinessSignals): ApexReadine
196
210
  detail: signals.targetHealthy
197
211
  ? "apex target health check passed"
198
212
  : "apex target health check failing or unestablished",
213
+ remedy:
214
+ "Cleared by the same www-publish evidence run as Dual-run parity — every route serving " +
215
+ "correctly on CloudFront is what proves the target healthy.",
199
216
  });
200
217
 
201
- // 5. Evidence freshness — the parity evidence is tied to the CURRENT live digest
202
- // (`static-publish-fails-closed-reviewed-sha`). Stale evidence (a different
203
- // digest) or a missing digest on either side fails closed.
218
+ // 5. Evidence freshness — the parity evidence is tied to the CURRENT live digest.
219
+ // Stale evidence (a different digest) or a missing digest on either side fails
220
+ // closed.
204
221
  const evidenceDigest = dr?.digest ?? null;
205
222
  const freshOk =
206
223
  !!evidenceDigest && !!signals.liveDigest && evidenceDigest === signals.liveDigest;
@@ -216,6 +233,8 @@ export function computeApexReadiness(signals: ApexReadinessSignals): ApexReadine
216
233
  ? "parity evidence carries no digest — cannot confirm it matches the live tree"
217
234
  : `parity evidence is stale: it was produced against ${short(evidenceDigest)}, ` +
218
235
  `not the live digest ${short(signals.liveDigest)}`,
236
+ remedy:
237
+ "Re-publish tokenoftrust.com so the dual-run evidence is re-tied to the current live digest.",
219
238
  });
220
239
 
221
240
  // 6. Captured revert target — a rollback must have persisted prior DNS records to
@@ -230,6 +249,9 @@ export function computeApexReadiness(signals: ApexReadinessSignals): ApexReadine
230
249
  signals.revert.capturedAt ? ` @ ${signals.revert.capturedAt}` : ""
231
250
  })`
232
251
  : "no captured revert target — prior DNS records are not persisted, so there is no tested rollback",
252
+ remedy:
253
+ "Run a rehearsal (dry-run connect) from the Domain section — it captures the current DNS " +
254
+ "records as the rollback target without changing live DNS.",
233
255
  });
234
256
 
235
257
  const blockedReasons = checks.filter((c) => !c.ok).map((c) => c.detail);
@@ -267,9 +289,10 @@ function num(v: unknown): number | null {
267
289
 
268
290
  /**
269
291
  * The consumed readiness-evidence record the go-live dual-run/health CI persists
270
- * per tenant. u9 READS it; it does not produce it. Bundles the three signals u9
292
+ * per tenant. Bundles the three signals the gate
271
293
  * cannot derive from `DomainState` alone: the dual-run parity result, the apex
272
294
  * target's health, and the digest currently live (so freshness can be checked).
295
+ * The gate READS it; it does not produce it.
273
296
  */
274
297
  export interface ReadinessEvidence {
275
298
  dualRun: DualRunEvidence | null;
@@ -311,7 +334,7 @@ export interface ReadinessDomainState {
311
334
 
312
335
  /**
313
336
  * Assemble the {@link ApexReadinessSignals} the gate decides over from the three
314
- * live sources — the owner capability (u10), the per-tenant domain state (revert +
337
+ * live sources — the owner capability, the per-tenant domain state (revert +
315
338
  * in-flight), and the consumed evidence record — PURE so the endpoint's mapping is
316
339
  * unit-testable without KV/session I/O. The endpoint does the reads; this does the
317
340
  * mapping.
@@ -1,11 +1,10 @@
1
1
  /**
2
- * Dispatch-health assessment (dg5) — is the static-publish `repository_dispatch`
2
+ * Dispatch-health assessment — is the static-publish `repository_dispatch`
3
3
  * path actually WIRED and HEALTHY, or would a "go-live" silently hand the publish
4
4
  * to a dead-end? The Worker fires a `repository_dispatch` and gets a 204 back
5
5
  * having verified NOTHING about the target (see `createRepositoryDispatchSink` —
6
6
  * it is fire-and-forget by construction). This module is the HONEST pre-flight
7
- * surface that answers, per binding decision `static-publish-fails-closed-
8
- * reviewed-sha`, whether that path can carry a publish at all — reporting each
7
+ * surface that answers whether that path can carry a publish at all — reporting each
9
8
  * concrete signal pass/fail/unknown with a reason, never a silent assumption.
10
9
  *
11
10
  * FOUR SIGNALS:
@@ -19,14 +18,13 @@
19
18
  * that lives only on the deploy branch is
20
19
  * NOT yet reachable. A 404 here is the
21
20
  * honest "not yet promoted to main" state
22
- * (the HELD main-promotion gate,
23
- * `pending-main-promotion-and-production-
24
- * publish-activation`) — surfaced, not papered.
21
+ * (the HELD main-promotion gate) —
22
+ * surfaced, not papered.
25
23
  * - `target-reachable` — does the published `live/manifest.json`
26
24
  * answer? DIAGNOSTIC ONLY: a pre-first-
27
25
  * publish target legitimately has none, so
28
26
  * this never blocks a ship (the post-merge
29
- * VERIFY in dg4's orchestrator is the real
27
+ * VERIFY in the orchestrator is the real
30
28
  * no-false-live gate).
31
29
  *
32
30
  * FAIL-CLOSED SEMANTICS:
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Apex domain-connect state + dispatch (handoff 2026-08-05-domain-connect-ux).
2
+ * Apex domain-connect state + dispatch.
3
3
  *
4
4
  * The admin Publish tab's "Domain" section drives the apex DNS cutover
5
5
  * (tokenoftrust.com → CloudFront) and its rollback as PRODUCT actions, not a
@@ -212,8 +212,11 @@ export interface DomainRunCallback {
212
212
  * overwrite an existing pointer with null — the FIRST capture is
213
213
  * the true pre-platform state)
214
214
  * rollback → `pantheon`
215
- * Rehearsal (wire: dryRun) runs never move state — only the run outcome is
216
- * recorded.
215
+ * Rehearsal (wire: dryRun) runs never move `state`but a rehearsal CONNECT
216
+ * that reports a `priorKey` DOES record the revert target (prior-records pointer),
217
+ * because persisting a metadata capture mutates no live DNS, and it's what lets
218
+ * the apex-cutover readiness gate's "revert captured" precondition go green BEFORE
219
+ * the real cutover. Capturing the revert target is decoupled from moving state.
217
220
  */
218
221
  export function completeDomainRun(
219
222
  state: DomainState,
@@ -245,19 +248,25 @@ export function completeDomainRun(
245
248
  };
246
249
 
247
250
  let next: DomainState = { ...state, lastRun: updatedRun };
248
- if (cb.status === "succeeded" && !run.rehearsal) {
249
- const reported =
250
- cb.state === "cloudfront" || cb.state === "pantheon" ? cb.state : null;
251
- next = {
252
- ...next,
253
- state: reported ?? (run.action === "connect" ? "cloudfront" : "pantheon"),
254
- since: updatedRun.finishedAt,
255
- };
251
+ if (cb.status === "succeeded") {
252
+ // Platform state flips only on a REAL (non-rehearsal) run — a rehearsal
253
+ // changes no DNS, so it must never claim the apex moved.
254
+ if (!run.rehearsal) {
255
+ const reported =
256
+ cb.state === "cloudfront" || cb.state === "pantheon" ? cb.state : null;
257
+ next = {
258
+ ...next,
259
+ state: reported ?? (run.action === "connect" ? "cloudfront" : "pantheon"),
260
+ since: updatedRun.finishedAt,
261
+ };
262
+ }
263
+ // Capturing the revert target is DECOUPLED from moving state: any successful
264
+ // connect that carries a priorKey records it — a rehearsal included — so the
265
+ // readiness gate can confirm a tested rollback before the real cutover. Never
266
+ // regress an existing pointer to null (the FIRST capture is the true origin).
256
267
  if (run.action === "connect") {
257
268
  next = {
258
269
  ...next,
259
- // Keep the FIRST capture if the callback carries none — never regress
260
- // the pointer to null once a real capture exists.
261
270
  priorRecordsKey: cb.priorKey ?? state.priorRecordsKey,
262
271
  priorCapturedAt: cb.priorKey
263
272
  ? (cb.completedAt ?? at)
@@ -0,0 +1,78 @@
1
+ // Client-side polling primitives for the admin Publish tab: a headSha-keyed
2
+ // cache and an overlap coalescer. The tab's `is:inline` island can't import
3
+ // modules, so it hand-mirrors these (see AdminPublishTab.astro `makeKeyedCache`
4
+ // / `makeCoalescer`); this module is the tested source of truth for the
5
+ // behaviour — keep the mirror in sync.
6
+
7
+ /**
8
+ * A cache keyed by an opaque string. The Publish tab keys panel data by a
9
+ * fingerprint that includes candidate head shas + the live version, so a hit
10
+ * means "this panel's data at this state is already known — don't re-fetch".
11
+ * A candidate's data at a fixed head sha is immutable, so cached values never
12
+ * go stale for the same key.
13
+ */
14
+ export function createKeyedCache<T>() {
15
+ const store = new Map<string, T>();
16
+ return {
17
+ get(key: string): T | undefined {
18
+ return store.get(key);
19
+ },
20
+ has(key: string): boolean {
21
+ return store.has(key);
22
+ },
23
+ set(key: string, value: T): void {
24
+ store.set(key, value);
25
+ },
26
+ delete(key: string): void {
27
+ store.delete(key);
28
+ },
29
+ clear(): void {
30
+ store.clear();
31
+ },
32
+ get size(): number {
33
+ return store.size;
34
+ },
35
+ };
36
+ }
37
+
38
+ export type KeyedCache<T> = ReturnType<typeof createKeyedCache<T>>;
39
+
40
+ /**
41
+ * Coalesce overlapping async runs so rapid re-renders, poll ticks, and user
42
+ * actions can't stack duplicate/overlapping requests for the same panel. While
43
+ * a run is in flight, further triggers don't start a second concurrent run —
44
+ * they mark the run "pending" so exactly ONE trailing run fires after the
45
+ * current one settles (regardless of how many triggers arrived meanwhile).
46
+ */
47
+ export function createCoalescer(run: () => Promise<void> | void) {
48
+ let inFlight = false;
49
+ let pending = false;
50
+
51
+ const tick = async (): Promise<void> => {
52
+ if (inFlight) {
53
+ pending = true;
54
+ return;
55
+ }
56
+ inFlight = true;
57
+ try {
58
+ await run();
59
+ } finally {
60
+ inFlight = false;
61
+ if (pending) {
62
+ pending = false;
63
+ void tick();
64
+ }
65
+ }
66
+ };
67
+
68
+ return {
69
+ trigger(): Promise<void> {
70
+ return tick();
71
+ },
72
+ get busy(): boolean {
73
+ return inFlight;
74
+ },
75
+ };
76
+ }
77
+
78
+ export type Coalescer = ReturnType<typeof createCoalescer>;
@@ -1,33 +1,32 @@
1
1
  /**
2
- * Ship operations workspace (unit dg7) — the PURE composer behind the admin
2
+ * Ship operations workspace — the PURE composer behind the admin
3
3
  * Publish tab's operations surface that sits ALONGSIDE the go-live core
4
4
  * (AdminPublishTab.astro's change queue + accept/reject + dispatch status).
5
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:
6
+ * This module composes the operations workspace ALONGSIDE the Publish-tab core
7
+ * (the change queue from GET /api/changes, Go-live/Reject, the live-version +
8
+ * dispatch-status panel, and the owner-only Domain section) the three read/act
9
+ * surfaces woven into the same tab:
11
10
  *
12
11
  * 1. DIFF VIEW — for one pending candidate, the artifact-level diff of what
13
12
  * 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
13
+ * tied to the exact tenant Gitea head SHA, with the immutable
14
+ * `/<tenant>/preview/rev/<sha>/` link and the evidence AUTHORITY
16
15
  * (authoritative / stale / missing) — never a diff a reviewer would mistake
17
16
  * for the head when a last-good version actually renders.
18
17
  * 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
18
+ * (`listPromotions`, promotion_status), each row carrying the tenant
19
+ * Gitea SHA, its promotion time, a
21
20
  * `/rev/<sha>/` link, and — marking the current live one.
22
21
  * 3. ROLLBACK — pick a prior version and roll back THROUGH the one orchestrator
23
- * (`/api/changes/rollback` → `rollbackCandidateAtomic` / u8's
22
+ * (`/api/changes/rollback` → `rollbackCandidateAtomic` /
24
23
  * `staticRollbackOrchestrator`), rendering only the HONEST terminal state
25
24
  * (`rolled_back` when VERIFIED; `publish_pending` in flight; `publish_failed`
26
25
  * / `blocked` fail-closed) — never a false "rolled back".
27
26
  *
28
27
  * COMPOSES, never rebuilds:
29
- * - the u5 immutable link is `reviewDashboardRevisionHref` verbatim;
30
- * - the dg3 authority is `candidateEvidenceAuthority` /
28
+ * - the immutable link is `reviewDashboardRevisionHref` verbatim;
29
+ * - the authority is `candidateEvidenceAuthority` /
31
30
  * `classifyCandidateForRender` verbatim;
32
31
  * - the versions come from the SAME `KVVersionStore.listPromotions` the
33
32
  * "one step back" rollback resolution reads;
@@ -38,16 +37,13 @@
38
37
  * decision is unit-tested without a Worker/KV — mirroring the reviewDashboard.ts /
39
38
  * candidatePreviewState.ts split.
40
39
  *
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.
40
+ * `rollbackMode` names the path a rollback is honestly driven by: a `control-plane`
41
+ * (hosted) tenant and a `pointer` (static-publish) tenant BOTH roll back one-click
42
+ * via the instant KV `live` pointer flip the Worker serves whatever bundle the
43
+ * pointer's digest names (from R2, for a static tenant), so restoring the pointer IS
44
+ * the rollback. The `publish_pending`/`publish_failed`/`blocked` honesty model from
45
+ * `interpretRollbackResponse` below applies identically in both modes only the
46
+ * digest column (surfaced for a static tenant) differs.
51
47
  */
52
48
  import { reviewDashboardRevisionHref } from "@/lib/preview/reviewDashboard";
53
49
  import {
@@ -60,10 +56,36 @@ import type { ReviewEnvironment, PromotionEvent } from "@tot/public-runtime";
60
56
 
61
57
  // ── Versions / promotion history ───────────────────────────────────────────
62
58
 
63
- /** One published version in the tenant's promotion history (u2/u3 promotion_status). */
59
+ /** The plain-language "what shipped in this version" model (same shape the staged
60
+ * release summary uses — a lead sentence + one highlight per included change). */
61
+ export interface VersionReleaseSummary {
62
+ lead: string;
63
+ highlights: { title: string; detail: string }[];
64
+ }
65
+
66
+ /** One change that shipped in a published version — its PR summary, for the
67
+ * version's openable "what shipped" list. Fields degrade to null/`PR #N` when
68
+ * candidate_summary didn't cover the PR (best-effort enrichment). */
69
+ export interface WorkspaceVersionChange {
70
+ changeId: string;
71
+ prNumber: number | null;
72
+ headSha: string;
73
+ title: string | null;
74
+ description: string | null;
75
+ fileCount: number | null;
76
+ linesAdded: number | null;
77
+ linesRemoved: number | null;
78
+ }
79
+
80
+ /** Per-version release enrichment, keyed by versionId (== the promoted aggregate sha). */
81
+ export interface VersionReleaseInfo {
82
+ releaseSummary: VersionReleaseSummary | null;
83
+ changes: WorkspaceVersionChange[];
84
+ }
85
+
86
+ /** One published version in the tenant's promotion history (promotion_status). */
64
87
  export interface WorkspaceVersion {
65
- /** The tenant Gitea SHA the version serves — the content identity
66
- * (`gitea-only-tenant-content-authority`). */
88
+ /** The tenant Gitea SHA the version serves — the content identity. */
67
89
  versionId: string;
68
90
  /** How the version was published (a Gitea commit, or a content hash). */
69
91
  source: "commit" | "hash" | null;
@@ -80,14 +102,33 @@ export interface WorkspaceVersion {
80
102
  * digest lives only in the S3 archive the Worker can't enumerate. Never fabricated.
81
103
  */
82
104
  digest: string | null;
83
- /** The u5 immutable `/preview/<tenant>/rev/<sha>/` deep-link, or `null` without a SHA. */
105
+ /** The immutable `/<tenant>/preview/rev/<sha>/` deep-link, or `null` without a SHA. */
84
106
  revHref: string | null;
85
107
  /** The version currently serving live (the one you never roll back TO). */
86
108
  isCurrent: boolean;
109
+ /**
110
+ * The plain-language "what shipped in this version" summary, composed from the
111
+ * release receipt's included PRs + their candidate_summary. `null` when no
112
+ * receipt links this version to a PR set, or none carried summary prose.
113
+ */
114
+ releaseSummary: VersionReleaseSummary | null;
115
+ /**
116
+ * The PRs that shipped in this version (from its release receipt), each with its
117
+ * human summary — the version's openable "what shipped" list. Empty when no
118
+ * receipt links this version to a PR set.
119
+ */
120
+ changes: WorkspaceVersionChange[];
121
+ /** How many changes shipped in this version, or `null` when unknown (no receipt). */
122
+ changeCount: number | null;
87
123
  }
88
124
 
89
- /** How a rollback is honestly driven for this tenant (see the module HONEST BOUNDARY note). */
90
- export type RollbackMode = "control-plane" | "static-archive";
125
+ /**
126
+ * How a rollback is honestly driven for this tenant. `control-plane` (hosted) and
127
+ * `pointer` (static-publish) both roll back one-click via the instant KV `live`
128
+ * pointer flip; they render identical instant "Roll back" copy, differing only in
129
+ * whether the static digest column is shown.
130
+ */
131
+ export type RollbackMode = "control-plane" | "pointer";
91
132
 
92
133
  /** The versions-panel view model. */
93
134
  export interface VersionsView {
@@ -98,9 +139,9 @@ export interface VersionsView {
98
139
  /** Whether the tenant publishes its public site to an external static target. */
99
140
  staticPublish: boolean;
100
141
  /**
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).
142
+ * `control-plane` (hosted) and `pointer` (static-publish) both roll back
143
+ * one-click via `targetVersionId` (instant KV pointer flip) and render identical
144
+ * instant "Roll back" copy.
104
145
  */
105
146
  rollbackMode: RollbackMode;
106
147
  }
@@ -122,6 +163,12 @@ export interface BuildVersionsViewInput {
122
163
  currentDigest: string | null;
123
164
  /** Per-version manifest facts, keyed by versionId (absent ⇒ unknown facts). */
124
165
  manifests: Map<string, VersionManifestFacts>;
166
+ /**
167
+ * Per-version release enrichment, keyed by versionId (== the promoted aggregate
168
+ * sha on the release receipt). Absent ⇒ a version with no linked PR set (an
169
+ * honest empty release summary + no changes list — never fabricated).
170
+ */
171
+ releases?: Map<string, VersionReleaseInfo>;
125
172
  staticPublish: boolean;
126
173
  }
127
174
 
@@ -129,13 +176,22 @@ export interface BuildVersionsViewInput {
129
176
  * Assemble the versions panel from the promotion history + per-version manifest
130
177
  * facts + the current-live pointer. Pure. The current live row is marked; the
131
178
  * digest is populated ONLY for the current live of a static tenant (never faked
132
- * for priors). `revHref` is the u5 immutable link.
179
+ * for priors). `revHref` is the immutable link.
133
180
  */
134
181
  export function buildVersionsView(input: BuildVersionsViewInput): VersionsView {
135
- const { tenantId, promotions, currentVersionId, currentDigest, manifests, staticPublish } = input;
182
+ const {
183
+ tenantId,
184
+ promotions,
185
+ currentVersionId,
186
+ currentDigest,
187
+ manifests,
188
+ releases,
189
+ staticPublish,
190
+ } = input;
136
191
 
137
192
  const versions: WorkspaceVersion[] = promotions.map((p) => {
138
193
  const facts = manifests.get(p.versionId);
194
+ const release = releases?.get(p.versionId);
139
195
  const isCurrent = currentVersionId != null && p.versionId === currentVersionId;
140
196
  return {
141
197
  versionId: p.versionId,
@@ -147,6 +203,9 @@ export function buildVersionsView(input: BuildVersionsViewInput): VersionsView {
147
203
  digest: isCurrent && staticPublish ? currentDigest : null,
148
204
  revHref: reviewDashboardRevisionHref(tenantId, p.versionId),
149
205
  isCurrent,
206
+ releaseSummary: release?.releaseSummary ?? null,
207
+ changes: release?.changes ?? [],
208
+ changeCount: release ? release.changes.length : null,
150
209
  };
151
210
  });
152
211
 
@@ -154,7 +213,7 @@ export function buildVersionsView(input: BuildVersionsViewInput): VersionsView {
154
213
  current: { versionId: currentVersionId, digest: staticPublish ? currentDigest : null },
155
214
  versions,
156
215
  staticPublish,
157
- rollbackMode: staticPublish ? "static-archive" : "control-plane",
216
+ rollbackMode: staticPublish ? "pointer" : "control-plane",
158
217
  };
159
218
  }
160
219
 
@@ -175,13 +234,13 @@ export interface CandidateDiffView {
175
234
  headSha: string | null;
176
235
  /** The immutable version actually rendered + diffed — the reconcile ACTUAL. */
177
236
  versionId: string | null;
178
- /** The u5 immutable link to the exact reviewed SHA, or `null` without a head SHA. */
237
+ /** The immutable link to the exact reviewed SHA, or `null` without a head SHA. */
179
238
  revHref: string | null;
180
- /** dg3 evidence authority: authoritative (tied to head) / stale (last-good) / missing. */
239
+ /** Evidence authority: authoritative (tied to head) / stale (last-good) / missing. */
181
240
  authority: CandidateEvidenceAuthority;
182
241
  /** The artifact-level diff (candidate rendered version vs current live), sorted by path. */
183
242
  files: DiffEntry[];
184
- /** True when the candidate has a renderable version to diff (dg3 `ready`). */
243
+ /** True when the candidate has a renderable version to diff (the `ready` classification). */
185
244
  renderable: boolean;
186
245
  /** When NOT renderable, the honest reason (names why) — never a fake empty diff. */
187
246
  blockedReason: string | null;
@@ -199,7 +258,7 @@ export interface BuildCandidateDiffInput {
199
258
  /**
200
259
  * Diff a candidate's rendered version against the current live version, at the
201
260
  * artifact level (each path carries a content hash in its site-version manifest).
202
- * Composes dg3's `classifyCandidateForRender` (only a `ready` candidate has a
261
+ * Composes `classifyCandidateForRender` (only a `ready` candidate has a
203
262
  * version to diff — a blocked/queued one yields an HONEST `blockedReason`, never a
204
263
  * fabricated empty diff) and `candidateEvidenceAuthority` (so a diff of a last-good
205
264
  * version that LAGS the head is flagged `stale`, not passed off as the head).
@@ -282,10 +341,9 @@ export interface RollbackOutcome {
282
341
  * for BOTH planes without the surface having to know which it hit:
283
342
  * - a HOSTED (control-plane) rollback returns `{ rolledBack: true, toVersionId }`
284
343
  * on success (the pointer flip IS the rollback) or `{ error, reason }` on refusal;
285
- * - a STATIC rollback returns the orchestrator's honest `{ state, ... }` (u8).
344
+ * - a STATIC rollback returns the orchestrator's honest `{ state, ... }`.
286
345
  * 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`).
346
+ * `rolled_back` — a `publish_pending` is NEVER rendered as rolled back.
289
347
  * Pure.
290
348
  */
291
349
  export function interpretRollbackResponse(