@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.
- package/apps/storefront/astro.config.mjs +17 -1
- package/apps/storefront/integrations/materialize-guard.mjs +56 -0
- package/apps/storefront/migrations-apps/0001_woozy_lyja.sql +22 -0
- package/apps/storefront/migrations-apps/0002_good_switch.sql +26 -0
- package/apps/storefront/migrations-apps/0003_mute_marauders.sql +12 -0
- package/apps/storefront/migrations-apps/meta/0001_snapshot.json +990 -0
- package/apps/storefront/migrations-apps/meta/0002_snapshot.json +1160 -0
- package/apps/storefront/migrations-apps/meta/0003_snapshot.json +1235 -0
- package/apps/storefront/migrations-apps/meta/_journal.json +21 -0
- package/apps/storefront/package.json +0 -1
- package/apps/storefront/public/js/dashboard-team.js +38 -2
- package/apps/storefront/src/components/Seo.astro +65 -1
- package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +878 -0
- package/apps/storefront/src/components/admin/AdminBlogTab.astro +1432 -0
- package/apps/storefront/src/components/admin/AdminCustomersTab.astro +3 -7
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +1756 -1382
- package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +1 -3
- package/apps/storefront/src/components/blog/BlogPaywall.astro +87 -0
- package/apps/storefront/src/components/blog/PostAccessBadge.astro +43 -0
- package/apps/storefront/src/components/blog/PostGrid.astro +57 -0
- package/apps/storefront/src/components/blog/ReactionBar.astro +214 -0
- package/apps/storefront/src/layouts/Layout.astro +48 -3
- package/apps/storefront/src/lib/activity/alerts.ts +426 -0
- package/apps/storefront/src/lib/activity/changeActorAttribution.ts +85 -0
- package/apps/storefront/src/lib/activity/deployVersion.ts +120 -0
- package/apps/storefront/src/lib/activity/ingest.ts +188 -0
- package/apps/storefront/src/lib/activity/ingestAuth.ts +105 -0
- package/apps/storefront/src/lib/activity/killSwitch.ts +80 -0
- package/apps/storefront/src/lib/activity/query.ts +403 -0
- package/apps/storefront/src/lib/activity/recordActivity.ts +101 -0
- package/apps/storefront/src/lib/activity/store.ts +120 -0
- package/apps/storefront/src/lib/activity/uiActor.ts +150 -0
- package/apps/storefront/src/lib/activity/workerCommit.ts +70 -0
- package/apps/storefront/src/lib/admin/adminShell.ts +290 -0
- package/apps/storefront/src/lib/admin/ai/approvalMode.ts +2 -3
- package/apps/storefront/src/lib/admin/ai/audit.ts +3 -5
- package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +7 -7
- package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +3 -3
- package/apps/storefront/src/lib/admin/orderClient.ts +3 -5
- package/apps/storefront/src/lib/admin/services/orders.ts +17 -19
- package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +4 -6
- package/apps/storefront/src/lib/admin/viewmodels/settings.ts +1 -1
- package/apps/storefront/src/lib/apps/adminService.ts +4 -4
- package/apps/storefront/src/lib/apps/adminSession.ts +53 -44
- package/apps/storefront/src/lib/apps/apiAuth.ts +2 -2
- package/apps/storefront/src/lib/apps/apiRoute.ts +2 -2
- package/apps/storefront/src/lib/apps/catalogMapper.ts +1 -1
- package/apps/storefront/src/lib/apps/credentials.ts +2 -2
- package/apps/storefront/src/lib/apps/gatewayKeys.ts +4 -4
- package/apps/storefront/src/lib/apps/healthAggregate.ts +5 -5
- package/apps/storefront/src/lib/apps/orders/attributionService.ts +2 -2
- package/apps/storefront/src/lib/apps/orders/customerHash.ts +1 -1
- package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +12 -12
- package/apps/storefront/src/lib/apps/orders/idempotency.ts +7 -7
- package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +1 -1
- package/apps/storefront/src/lib/apps/orders/orderMapper.ts +2 -2
- package/apps/storefront/src/lib/apps/orders/ordersStore.ts +6 -6
- package/apps/storefront/src/lib/apps/registryService.ts +6 -6
- package/apps/storefront/src/lib/apps/scopes.ts +2 -2
- package/apps/storefront/src/lib/apps/tokenIssuer.ts +4 -4
- package/apps/storefront/src/lib/apps/tokenVerifier.ts +6 -6
- package/apps/storefront/src/lib/apps/widgets/eligibility.ts +2 -2
- package/apps/storefront/src/lib/apps/widgets/frameProps.ts +3 -3
- package/apps/storefront/src/lib/apps/widgets/launchToken.ts +4 -4
- package/apps/storefront/src/lib/apps/widgets/placements.ts +2 -2
- package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +1 -1
- package/apps/storefront/src/lib/auth/adminEntry.ts +9 -9
- package/apps/storefront/src/lib/auth/identityToken.ts +6 -14
- package/apps/storefront/src/lib/auth/loginGate.ts +75 -13
- package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +6 -4
- package/apps/storefront/src/lib/auth/operatorSession.ts +113 -0
- package/apps/storefront/src/lib/auth/route.ts +44 -4
- package/apps/storefront/src/lib/auth/session.ts +7 -8
- package/apps/storefront/src/lib/auth/totAccessClient.ts +8 -18
- package/apps/storefront/src/lib/basePath.ts +9 -2
- package/apps/storefront/src/lib/blog/access.ts +29 -0
- package/apps/storefront/src/lib/blog/collection.ts +313 -0
- package/apps/storefront/src/lib/blog/markdown.ts +174 -0
- package/apps/storefront/src/lib/blog/provider.ts +5 -0
- package/apps/storefront/src/lib/blog/reactions.ts +87 -0
- package/apps/storefront/src/lib/blog/rss.ts +92 -0
- package/apps/storefront/src/lib/blog/teaser.ts +83 -0
- package/apps/storefront/src/lib/blog/types.ts +34 -0
- package/apps/storefront/src/lib/blog/viewCounts.ts +243 -0
- package/apps/storefront/src/lib/d1/catalog.ts +2 -2
- package/apps/storefront/src/lib/d1/schema-apps.ts +147 -14
- package/apps/storefront/src/lib/dev/activityIngestToken.ts +2 -4
- package/apps/storefront/src/lib/dev/activityStore.ts +3 -5
- package/apps/storefront/src/lib/dev/apiBase.ts +1 -4
- package/apps/storefront/src/lib/dev/cockpitStore.ts +1 -9
- package/apps/storefront/src/lib/dev/devDraft.ts +1 -1
- package/apps/storefront/src/lib/dev/previewStatus.ts +9 -21
- package/apps/storefront/src/lib/dev/rendezvousBroker.ts +9 -24
- package/apps/storefront/src/lib/edgeCache.ts +17 -0
- package/apps/storefront/src/lib/env.ts +51 -0
- package/apps/storefront/src/lib/http/fetchTransport.ts +63 -0
- package/apps/storefront/src/lib/i18n.ts +25 -0
- package/apps/storefront/src/lib/jsonld.ts +118 -0
- package/apps/storefront/src/lib/monitoring/manifest.ts +11 -11
- package/apps/storefront/src/lib/newsletter/confirmationEmail.ts +30 -0
- package/apps/storefront/src/lib/newsletter/consent.ts +23 -0
- package/apps/storefront/src/lib/newsletter/digest.ts +117 -0
- package/apps/storefront/src/lib/newsletter/resendClient.ts +79 -0
- package/apps/storefront/src/lib/newsletter/send.ts +265 -0
- package/apps/storefront/src/lib/newsletter/subscribers.ts +156 -0
- package/apps/storefront/src/lib/publish/adminPublishTab.ts +3519 -0
- package/apps/storefront/src/lib/publish/apex-readiness.ts +41 -18
- package/apps/storefront/src/lib/publish/dispatchHealth.ts +5 -7
- package/apps/storefront/src/lib/publish/domainState.ts +22 -13
- package/apps/storefront/src/lib/publish/panelPolling.ts +78 -0
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +100 -42
- package/apps/storefront/src/lib/publish/stageBundleUpload.ts +215 -0
- package/apps/storefront/src/lib/rate/kvWindow.ts +53 -0
- package/apps/storefront/src/lib/search/index.ts +2 -2
- package/apps/storefront/src/lib/seo/alternates.ts +42 -0
- package/apps/storefront/src/lib/seo/meta.ts +65 -0
- package/apps/storefront/src/lib/social/golive.ts +212 -0
- package/apps/storefront/src/lib/social/notify.ts +32 -0
- package/apps/storefront/src/lib/social/tokenStore.ts +171 -0
- package/apps/storefront/src/lib/the-build/provider.ts +27 -0
- package/apps/storefront/src/lib/the-build/readingTime.ts +35 -0
- package/apps/storefront/src/lib/the-build/toc.ts +95 -0
- package/apps/storefront/src/lib/tot/ToTClient.ts +1 -1
- package/apps/storefront/src/lib/tot/d1Client.ts +2 -2
- package/apps/storefront/src/lib/tot/totClientInterface.ts +3 -4
- package/apps/storefront/src/lib/webhooks/deliveryStore.ts +16 -17
- package/apps/storefront/src/middleware/index.ts +130 -2
- package/apps/storefront/src/pages/admin/ops-timeline.astro +420 -0
- package/apps/storefront/src/pages/admin.astro +52 -1206
- package/apps/storefront/src/pages/api/activity.ts +137 -0
- package/apps/storefront/src/pages/api/admin/activity-alerts.ts +73 -0
- package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +27 -1
- package/apps/storefront/src/pages/api/apps/admin/install.ts +26 -1
- package/apps/storefront/src/pages/api/apps/admin/resume.ts +24 -1
- package/apps/storefront/src/pages/api/apps/admin/suspend.ts +24 -1
- package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +25 -1
- package/apps/storefront/src/pages/api/apps/admin/update.ts +24 -1
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +11 -0
- package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +11 -0
- package/apps/storefront/src/pages/api/apps/v1/attribution.ts +20 -0
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +12 -0
- package/apps/storefront/src/pages/api/blog/[collection]/[slug]/react.ts +104 -0
- package/apps/storefront/src/pages/api/cache-purge.ts +11 -0
- package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +30 -0
- package/apps/storefront/src/pages/api/newsletter/[collection]/subscribe.ts +146 -0
- package/apps/storefront/src/pages/api/newsletter/confirm.ts +75 -0
- package/apps/storefront/src/pages/api/newsletter/unsubscribe.ts +90 -0
- package/apps/storefront/src/pages/api/publish/stage-bundle.ts +70 -0
- package/apps/storefront/src/pages/api/rum/vitals.ts +21 -2
- package/apps/storefront/src/pages/auth/login.astro +5 -4
- package/apps/storefront/src/pages/blog/[slug].astro +96 -16
- package/apps/storefront/src/pages/blog/author/[author].astro +140 -0
- package/apps/storefront/src/pages/blog/category/[category].astro +67 -0
- package/apps/storefront/src/pages/blog/index.astro +2 -0
- package/apps/storefront/src/pages/blog/tag/[tag].astro +65 -0
- package/apps/storefront/src/pages/cockpit.astro +74 -12
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +15 -11
- package/apps/storefront/src/pages/search.astro +1 -1
- package/apps/storefront/src/pages/sitemap.xml.ts +20 -0
- package/apps/storefront/src/pages/the-build/[slug].astro +129 -0
- package/apps/storefront/src/pages/the-build/index.astro +146 -0
- package/apps/storefront/src/pages/the-build/rss.xml.ts +33 -0
- package/apps/storefront/src/styles/admin.css +1080 -0
- package/apps/storefront/tsconfig.json +5 -0
- package/package.json +1 -1
- package/packages/public-runtime/package.json +1 -0
- package/packages/public-runtime/src/activity/README.md +146 -0
- package/packages/public-runtime/src/activity/catalog.ts +553 -0
- package/packages/public-runtime/src/activity/event.ts +168 -0
- package/packages/public-runtime/src/activity/index.ts +16 -0
- package/packages/public-runtime/src/activity/redaction.ts +263 -0
- package/packages/public-runtime/src/candidate-index.ts +13 -0
- package/packages/public-runtime/src/catalog-d1.ts +1 -1
- package/packages/public-runtime/src/customization-preview.ts +25 -2
- package/packages/public-runtime/src/customization-runtime.ts +125 -3
- package/packages/public-runtime/src/customization-versioning.ts +10 -0
- package/packages/public-runtime/src/index.ts +7 -0
- package/packages/public-runtime/src/static-bundle.ts +357 -0
- package/packages/public-runtime/src/tenant.ts +36 -8
- package/scripts/build/copy-tenant-assets.mjs +14 -4
- package/scripts/dev/ai-edit.mjs +1 -1
- package/scripts/dev/checkout-watch.mjs +2 -2
- package/scripts/dev/file-browser.mjs +2 -4
- package/scripts/dev/file-writer.mjs +1 -1
- package/scripts/dev/git-status.mjs +2 -2
- package/scripts/dev/locate-handler.mjs +3 -3
- package/scripts/dev/port-check.mjs +3 -3
- package/scripts/dev/publish.mjs +5 -9
- package/scripts/dev/shot.mjs +1 -1
- package/scripts/dev/unified-diff.mjs +8 -3
- package/scripts/tot-dev.mjs +6 -9
- package/apps/storefront/src/lib/webhooks/signing.ts +0 -29
- package/apps/storefront/src/lib/webhooks/webhookSigningKey.ts +0 -146
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Machine-verifiable apex-cutover READINESS gate
|
|
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.
|
|
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;
|
|
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
|
-
* (
|
|
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).
|
|
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
|
|
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`
|
|
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
|
|
40
|
-
* wants).
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
//
|
|
203
|
-
//
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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 —
|
|
216
|
-
*
|
|
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"
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
15
|
-
*
|
|
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`,
|
|
20
|
-
* Gitea SHA
|
|
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` /
|
|
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
|
|
30
|
-
* - the
|
|
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
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* (
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
90
|
-
|
|
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`
|
|
102
|
-
*
|
|
103
|
-
*
|
|
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
|
|
179
|
+
* for priors). `revHref` is the immutable link.
|
|
133
180
|
*/
|
|
134
181
|
export function buildVersionsView(input: BuildVersionsViewInput): VersionsView {
|
|
135
|
-
const {
|
|
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 ? "
|
|
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
|
|
237
|
+
/** The immutable link to the exact reviewed SHA, or `null` without a head SHA. */
|
|
179
238
|
revHref: string | null;
|
|
180
|
-
/**
|
|
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 (
|
|
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
|
|
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, ... }
|
|
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(
|