@tokenoftrust/storefront-runner 2.0.2 → 2.0.3

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 (117) hide show
  1. package/package.json +1 -1
  2. package/scripts/lib/tenant-dirs.mjs +80 -0
  3. package/scripts/tenant/lint-embeds.mjs +180 -0
  4. package/scripts/tenant/materialize-tenant-content.mjs +533 -0
  5. package/scripts/tenant/validate.mjs +837 -0
  6. package/apps/storefront/scripts/build-v84-clean-clone-standalone.mjs +0 -68
  7. package/apps/storefront/scripts/deny-v84-clean-clone-standalone-capabilities.mjs +0 -111
  8. package/apps/storefront/scripts/deny-v84-dogfood-capabilities.mjs +0 -2
  9. package/apps/storefront/scripts/deny-v84-lifecycle-capabilities.mjs +0 -247
  10. package/apps/storefront/scripts/dogfood-v84-nonportable-routes.ts +0 -73
  11. package/apps/storefront/scripts/fixed-v84-native-install-producer.ts +0 -79
  12. package/apps/storefront/scripts/generated/v84-clean-clone-replay.bundle.mjs +0 -10982
  13. package/apps/storefront/scripts/import-v84-through-ws2.ts +0 -38
  14. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling-dependency.ts +0 -2
  15. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling.ts +0 -4
  16. package/apps/storefront/scripts/install-v84-native-through-ws1.ts +0 -185
  17. package/apps/storefront/scripts/prove-clone-isolation.ts +0 -506
  18. package/apps/storefront/scripts/replay-v84-clean-clone-pass.ts +0 -148
  19. package/apps/storefront/scripts/replay-v84-clean-clone-standalone.ts +0 -35
  20. package/apps/storefront/scripts/replay-v84-clean-clone.ts +0 -214
  21. package/apps/storefront/scripts/run-fixed-v84-native-install-producer.mjs +0 -18
  22. package/apps/storefront/scripts/run-v84-clone-isolation-proof.mjs +0 -18
  23. package/apps/storefront/scripts/run-v84-clone-lifecycle-proof.mjs +0 -18
  24. package/apps/storefront/scripts/run-v84-import-through-ws2-proof.mjs +0 -17
  25. package/apps/storefront/scripts/run-v84-nonportable-route-dogfood.mjs +0 -15
  26. package/apps/storefront/scripts/sanitize-v84-proof-environment.mjs +0 -21
  27. package/apps/storefront/scripts/v84CleanCloneGraphPin.ts +0 -42
  28. package/apps/storefront/scripts/v84CleanCloneSourceGraph.ts +0 -145
  29. package/apps/storefront/scripts/validate-v84-clone-lifecycle.ts +0 -18
  30. package/apps/storefront/src/components/admin/AdminAIWorkflowsTab.astro +0 -531
  31. package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +0 -878
  32. package/apps/storefront/src/components/admin/AdminBlogTab.astro +0 -1435
  33. package/apps/storefront/src/components/admin/AdminCheckoutSettingsPanel.astro +0 -226
  34. package/apps/storefront/src/components/admin/AdminCustomersTab.astro +0 -593
  35. package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +0 -465
  36. package/apps/storefront/src/components/admin/AdminIntegrationsTab.astro +0 -105
  37. package/apps/storefront/src/components/admin/AdminProductsTab.astro +0 -901
  38. package/apps/storefront/src/components/admin/AdminPublishTab.astro +0 -2983
  39. package/apps/storefront/src/components/admin/AdminReportingTab.astro +0 -435
  40. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +0 -782
  41. package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +0 -611
  42. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +0 -1223
  43. package/apps/storefront/src/lib/dev/cockpitStore.ts +0 -57
  44. package/apps/storefront/src/lib/publish/adminPublishTab.ts +0 -4625
  45. package/apps/storefront/src/lib/publish/apex-readiness.ts +0 -360
  46. package/apps/storefront/src/lib/publish/buildStream.ts +0 -269
  47. package/apps/storefront/src/lib/publish/collapsePersistence.ts +0 -72
  48. package/apps/storefront/src/lib/publish/dispatchHealth.ts +0 -267
  49. package/apps/storefront/src/lib/publish/domainState.ts +0 -360
  50. package/apps/storefront/src/lib/publish/lifecycleState.ts +0 -369
  51. package/apps/storefront/src/lib/publish/liveReleaseSummary.ts +0 -29
  52. package/apps/storefront/src/lib/publish/mergeDoctorOnFailure.ts +0 -78
  53. package/apps/storefront/src/lib/publish/mergeHealthBanner.ts +0 -62
  54. package/apps/storefront/src/lib/publish/mergeRowFindings.ts +0 -124
  55. package/apps/storefront/src/lib/publish/mergeSupportEscalation.ts +0 -75
  56. package/apps/storefront/src/lib/publish/panelPolling.ts +0 -78
  57. package/apps/storefront/src/lib/publish/publish-guard.ts +0 -88
  58. package/apps/storefront/src/lib/publish/publishFormat.ts +0 -165
  59. package/apps/storefront/src/lib/publish/publishHero.ts +0 -177
  60. package/apps/storefront/src/lib/publish/reviewRelease/changeState.ts +0 -66
  61. package/apps/storefront/src/lib/publish/reviewRelease/copy.ts +0 -141
  62. package/apps/storefront/src/lib/publish/reviewRelease/index.ts +0 -41
  63. package/apps/storefront/src/lib/publish/reviewRelease/permissions.ts +0 -47
  64. package/apps/storefront/src/lib/publish/reviewRelease/releaseState.ts +0 -42
  65. package/apps/storefront/src/lib/publish/reviewRelease/types.ts +0 -59
  66. package/apps/storefront/src/lib/publish/reviewRelease/viewModel.ts +0 -440
  67. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/actions.ts +0 -161
  68. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/controller.ts +0 -325
  69. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/render.ts +0 -241
  70. package/apps/storefront/src/lib/publish/shipWorkspace.ts +0 -439
  71. package/apps/storefront/src/lib/publish/sseRoute.ts +0 -119
  72. package/apps/storefront/src/lib/publish/stageBundleUpload.ts +0 -215
  73. package/apps/storefront/src/lib/publish/streamLogConsole.ts +0 -142
  74. package/apps/storefront/src/lib/publish/versionNumber.ts +0 -52
  75. package/apps/storefront/src/pages/admin/ops-timeline.astro +0 -420
  76. package/apps/storefront/src/pages/admin/review-release.astro +0 -265
  77. package/apps/storefront/src/pages/admin/select.astro +0 -96
  78. package/apps/storefront/src/pages/admin.astro +0 -570
  79. package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +0 -86
  80. package/apps/storefront/src/pages/api/apps/admin/gtm-migration-install.ts +0 -174
  81. package/apps/storefront/src/pages/api/apps/admin/gtm-migration.ts +0 -96
  82. package/apps/storefront/src/pages/api/apps/admin/health.ts +0 -44
  83. package/apps/storefront/src/pages/api/apps/admin/install.ts +0 -125
  84. package/apps/storefront/src/pages/api/apps/admin/integration-lifecycle.ts +0 -118
  85. package/apps/storefront/src/pages/api/apps/admin/integrations.ts +0 -89
  86. package/apps/storefront/src/pages/api/apps/admin/list.ts +0 -26
  87. package/apps/storefront/src/pages/api/apps/admin/platform-adapters.ts +0 -85
  88. package/apps/storefront/src/pages/api/apps/admin/resume.ts +0 -79
  89. package/apps/storefront/src/pages/api/apps/admin/suspend.ts +0 -80
  90. package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +0 -98
  91. package/apps/storefront/src/pages/api/apps/admin/update.ts +0 -126
  92. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +0 -70
  93. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +0 -51
  94. package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +0 -255
  95. package/apps/storefront/src/pages/api/apps/oauth/token.ts +0 -87
  96. package/apps/storefront/src/pages/api/apps/v1/attribution.ts +0 -162
  97. package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +0 -39
  98. package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +0 -49
  99. package/apps/storefront/src/pages/api/apps/v1/health.ts +0 -32
  100. package/apps/storefront/src/pages/api/apps/v1/inventory.ts +0 -59
  101. package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +0 -48
  102. package/apps/storefront/src/pages/api/apps/v1/orders.ts +0 -73
  103. package/apps/storefront/src/pages/api/apps/v1/reports.ts +0 -21
  104. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +0 -74
  105. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +0 -47
  106. package/apps/storefront/src/pages/api/cache-purge.ts +0 -55
  107. package/apps/storefront/src/pages/api/compliance/tamper.ts +0 -88
  108. package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +0 -154
  109. package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +0 -120
  110. package/apps/storefront/src/pages/api/integrations/v1/[operation].ts +0 -108
  111. package/apps/storefront/src/pages/api/publish/stage-bundle.ts +0 -70
  112. package/apps/storefront/src/pages/api/test/dev-session.ts +0 -133
  113. package/apps/storefront/src/pages/cockpit.astro +0 -1548
  114. package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +0 -119
  115. package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +0 -105
  116. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +0 -177
  117. package/apps/storefront/src/pages/dashboard/index.astro +0 -160
@@ -1,360 +0,0 @@
1
- /**
2
- * Machine-verifiable apex-cutover READINESS gate — the green
3
- * preconditions that MUST all pass before an owner apex (DNS) cutover is
4
- * permitted. It consumes the go-live apex machinery:
5
- * go-live owns the admin Domain Connect/Rollback button, the DNS cutover/rollback
6
- * CI (`www-domain.yml` → `cutover-dns.mjs`, prior-records persisted to
7
- * `s3://.../meta/`), and the HMAC completion callback; this gate CONSUMES those and
8
- * GATES them. Nothing here mutates DNS — it decides whether a cutover is ALLOWED.
9
- *
10
- * The gate is a PURE function over a signals bundle so the exact policy is
11
- * unit-testable independent of the runtime (mirrors `shipReadiness` in
12
- * `change/ship`'s CLI and the `acceptGate`/`apexCutoverGate` pure decisions). The
13
- * server endpoint (`GET /api/domain/readiness`) assembles the signals from the
14
- * live sources via {@link assembleApexReadinessSignals} — the owner capability
15
- * (`resolveShipCapabilities`), the per-tenant `DomainState` (in-flight run +
16
- * the CAPTURED revert target), and the consumed dual-run parity / target-health /
17
- * live-digest evidence record — and the admin Domain display + the `tot go-live`
18
- * CLI both read the SAME endpoint, so the enforcement gate and the surfaces never
19
- * diverge.
20
- *
21
- * EVIDENCE CONTRACT (the seam the go-live dual-run/health CI feeds): the parity,
22
- * target-health and live-digest signals are CONSUMED from a per-tenant evidence
23
- * record persisted at {@link readinessEvidenceKey} (KV). The gate does not RE-RUN
24
- * dual-run — it reads the record `scripts/publish/dual-run.mjs` + the health
25
- * probe produce. An absent/garbage record normalises fail-closed (no parity, no
26
- * digest, unhealthy), so readiness BLOCKS until the go-live CI writes real
27
- * evidence — never a false "ready" from a missing feed.
28
- *
29
- * FAIL-CLOSED throughout:
30
- * a missing, stale, or failed signal BLOCKS the cutover with an honest,
31
- * itemized reason — never a false "ready". Absence denies. The gate is
32
- * ADVISORY-vs-authoritative honest: it reports readiness, but the actual cutover
33
- * is still owner-gated at `apexCutoverGate`/`guardApexCutover` and executed
34
- * only by the go-live CI — a green readiness verdict never bypasses those.
35
- */
36
-
37
- /**
38
- * The consumed dual-run parity evidence (produced by `scripts/publish/dual-run.mjs`
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
- * gate dual-run itself enforces: zero CloudFront route failures. `statusMismatches`
42
- * are surfaced for review (content legitimately differs during a migration) but do
43
- * NOT block, exactly as `dual-run.mjs` treats them. `digest` ties the evidence to
44
- * the published bundle it validated, so freshness can be checked against the live
45
- * digest; `capturedAt` dates it.
46
- */
47
- export interface DualRunEvidence {
48
- /** Number of routes that FAILED on CloudFront (the hard gate — must be 0). */
49
- cfFailures: number;
50
- /** Status mismatches vs the baseline — surfaced for review, non-blocking. */
51
- statusMismatches: number;
52
- /** Total routes probed (context for the itemization; 0 ⇒ no real run). */
53
- routes: number;
54
- /** The published-bundle digest this evidence was produced against, or null. */
55
- digest: string | null;
56
- /** When the dual-run evidence was produced (ISO), or null. */
57
- capturedAt: string | null;
58
- }
59
-
60
- /** The captured revert target — prior DNS records persisted by a connect/rehearsal run. */
61
- export interface RevertTarget {
62
- /** S3 key of the persisted prior DNS records (`meta/dns-prior-records.*.json`), or null. */
63
- key: string | null;
64
- /** When they were captured (ISO), or null if never. */
65
- capturedAt: string | null;
66
- }
67
-
68
- /**
69
- * The concrete signals the readiness gate decides over. Assembled server-side
70
- * from the live sources; every field is fail-closed by construction (a null /
71
- * false / absent value denies its check).
72
- */
73
- export interface ApexReadinessSignals {
74
- /**
75
- * The acting principal holds the OWNER-ONLY apex-cutover capability
76
- * (`resolveShipCapabilities(principal).apexCutover`). A `ship-on-behalf`
77
- * developer or an unauthenticated caller is false → denied.
78
- */
79
- ownerCapable: boolean;
80
- /**
81
- * A domain connect/rollback run is currently in flight (`runInFlight(state)`).
82
- * A cutover cannot start while one is pending its CI callback — the exact race
83
- * `DomainState` exists to prevent.
84
- */
85
- runInFlight: boolean;
86
- /**
87
- * The consumed dual-run parity evidence, or null when none has been produced
88
- * (fail-closed: no evidence ⇒ not green).
89
- */
90
- dualRun: DualRunEvidence | null;
91
- /**
92
- * The apex target's health probe passed (the CloudFront/static origin is
93
- * serving). Distinct from parity; a false/failed probe blocks. Fail-closed
94
- * default false when it could not be established.
95
- */
96
- targetHealthy: boolean;
97
- /**
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
- * Null ⇒ freshness fails closed.
101
- */
102
- liveDigest: string | null;
103
- /** The captured revert target — a rollback must have somewhere to restore FROM. */
104
- revert: RevertTarget;
105
- }
106
-
107
- /** A stable id per readiness check, so surfaces can label/order them consistently. */
108
- export type ApexReadinessCheckId =
109
- | "owner-capability"
110
- | "no-run-in-flight"
111
- | "dual-run-parity"
112
- | "target-health"
113
- | "evidence-fresh"
114
- | "revert-captured";
115
-
116
- /** One itemized readiness check — ok/blocked plus an honest human-legible detail. */
117
- export interface ApexReadinessCheck {
118
- id: ApexReadinessCheckId;
119
- /** Short label for the surface (admin chip / CLI line). */
120
- label: string;
121
- /** True when this precondition is satisfied. */
122
- ok: boolean;
123
- /** Why it passed or, more importantly, why it's blocking — always actionable. */
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;
133
- }
134
-
135
- /** The full readiness verdict: ready iff EVERY check passed; itemized either way. */
136
- export interface ApexReadiness {
137
- /** True only when all checks pass — fail-closed on any missing/failed signal. */
138
- ready: boolean;
139
- /** Every check, in a stable order, whether it passed or blocked. */
140
- checks: ApexReadinessCheck[];
141
- /** The details of the blocking checks only (empty ⇒ ready). Convenience for surfaces. */
142
- blockedReasons: string[];
143
- }
144
-
145
- /**
146
- * Compute apex-cutover readiness from the signals — PURE, total, fail-closed. The
147
- * order is deliberate (owner → in-flight → parity → health → freshness → revert)
148
- * so the most fundamental blocker surfaces first, mirroring `shipReadiness`.
149
- *
150
- * The cutover is READY only when ALL checks pass. Any missing signal (null
151
- * evidence, null digest, absent revert key) is a BLOCK with an honest reason,
152
- * never a silent pass.
153
- */
154
- export function computeApexReadiness(signals: ApexReadinessSignals): ApexReadiness {
155
- const checks: ApexReadinessCheck[] = [];
156
-
157
- // 1. Owner-only capability. A non-owner is denied outright — this is the
158
- // same floor `apexCutoverGate` enforces, surfaced as a readiness item.
159
- checks.push({
160
- id: "owner-capability",
161
- label: "Owner capability",
162
- ok: signals.ownerCapable,
163
- detail: signals.ownerCapable
164
- ? "you hold the owner-only apex-cutover capability"
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.",
167
- });
168
-
169
- // 2. No run in flight — two concurrent DNS mutations is the race DomainState guards.
170
- checks.push({
171
- id: "no-run-in-flight",
172
- label: "No run in flight",
173
- ok: !signals.runInFlight,
174
- detail: signals.runInFlight
175
- ? "a domain connect/rollback run is already in flight — wait for it to complete"
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.",
178
- });
179
-
180
- // 3. Dual-run parity — zero CloudFront route failures (the hard gate dual-run.mjs
181
- // itself enforces). Missing evidence fails closed. Status mismatches are
182
- // surfaced but do NOT block (content legitimately differs mid-migration).
183
- const dr = signals.dualRun;
184
- const parityOk = !!dr && dr.routes > 0 && dr.cfFailures === 0;
185
- checks.push({
186
- id: "dual-run-parity",
187
- label: "Dual-run parity",
188
- ok: parityOk,
189
- detail: !dr
190
- ? "no dual-run parity evidence — run the CloudFront-vs-baseline dual-run first"
191
- : dr.routes === 0
192
- ? "dual-run evidence covered 0 routes — not a real parity run"
193
- : dr.cfFailures > 0
194
- ? `dual-run parity failing: ${dr.cfFailures} CloudFront route failure(s) of ${dr.routes}`
195
- : `dual-run green: 0 CloudFront failures across ${dr.routes} route(s)` +
196
- (dr.statusMismatches > 0
197
- ? ` (${dr.statusMismatches} status mismatch(es) surfaced for review — non-blocking)`
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).",
203
- });
204
-
205
- // 4. Target health — the apex target (CloudFront/static origin) is serving.
206
- checks.push({
207
- id: "target-health",
208
- label: "Target health",
209
- ok: signals.targetHealthy,
210
- detail: signals.targetHealthy
211
- ? "apex target health check passed"
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.",
216
- });
217
-
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.
221
- const evidenceDigest = dr?.digest ?? null;
222
- const freshOk =
223
- !!evidenceDigest && !!signals.liveDigest && evidenceDigest === signals.liveDigest;
224
- checks.push({
225
- id: "evidence-fresh",
226
- label: "Evidence fresh",
227
- ok: freshOk,
228
- detail: freshOk
229
- ? `evidence is tied to the live digest (${short(signals.liveDigest)})`
230
- : !signals.liveDigest
231
- ? "no live digest to tie the evidence to"
232
- : !evidenceDigest
233
- ? "parity evidence carries no digest — cannot confirm it matches the live tree"
234
- : `parity evidence is stale: it was produced against ${short(evidenceDigest)}, ` +
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.",
238
- });
239
-
240
- // 6. Captured revert target — a rollback must have persisted prior DNS records to
241
- // restore FROM. No captured target ⇒ the cutover has no tested one-line revert.
242
- const revertOk = !!signals.revert.key;
243
- checks.push({
244
- id: "revert-captured",
245
- label: "Revert captured",
246
- ok: revertOk,
247
- detail: revertOk
248
- ? `captured revert target present (prior DNS records${
249
- signals.revert.capturedAt ? ` @ ${signals.revert.capturedAt}` : ""
250
- })`
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.",
255
- });
256
-
257
- const blockedReasons = checks.filter((c) => !c.ok).map((c) => c.detail);
258
- return { ready: blockedReasons.length === 0, checks, blockedReasons };
259
- }
260
-
261
- /** Normalise a raw `dual-run.mjs --json` summary into {@link DualRunEvidence}. Pure.
262
- * Tolerant of the shape the script prints (`{ summary: { routes, cfFailures,
263
- * statusMismatches } }`) as well as a flattened record; returns null for anything
264
- * unrecognisable so the gate fails closed on garbage. `digest`/`capturedAt` are
265
- * read from the wrapping evidence record (the script itself doesn't stamp them). */
266
- export function normalizeDualRunEvidence(raw: unknown): DualRunEvidence | null {
267
- if (!raw || typeof raw !== "object") return null;
268
- const o = raw as Record<string, unknown>;
269
- const summary =
270
- o.summary && typeof o.summary === "object" ? (o.summary as Record<string, unknown>) : o;
271
- const cfFailures = num(summary.cfFailures);
272
- const routes = num(summary.routes);
273
- const statusMismatches = num(summary.statusMismatches);
274
- if (cfFailures === null || routes === null) return null;
275
- return {
276
- cfFailures,
277
- routes,
278
- statusMismatches: statusMismatches ?? 0,
279
- digest: typeof o.digest === "string" ? o.digest : null,
280
- capturedAt: typeof o.capturedAt === "string" ? o.capturedAt : null,
281
- };
282
- }
283
-
284
- function num(v: unknown): number | null {
285
- return typeof v === "number" && Number.isFinite(v) ? v : null;
286
- }
287
-
288
- // ─── Evidence record + signal assembly (the endpoint's non-pure boundary) ─────────
289
-
290
- /**
291
- * The consumed readiness-evidence record the go-live dual-run/health CI persists
292
- * per tenant. Bundles the three signals the gate
293
- * cannot derive from `DomainState` alone: the dual-run parity result, the apex
294
- * target's health, and the digest currently live (so freshness can be checked).
295
- * The gate READS it; it does not produce it.
296
- */
297
- export interface ReadinessEvidence {
298
- dualRun: DualRunEvidence | null;
299
- targetHealthy: boolean;
300
- liveDigest: string | null;
301
- }
302
-
303
- /** KV key for a tenant's consumed readiness-evidence record. */
304
- export function readinessEvidenceKey(tenantId: string): string {
305
- return `www-domain:readiness-evidence:${tenantId}`;
306
- }
307
-
308
- /**
309
- * Normalise a raw persisted evidence record into {@link ReadinessEvidence},
310
- * FAIL-CLOSED: anything unrecognisable yields no parity, an unhealthy target, and
311
- * no live digest — so the gate blocks rather than trusting a malformed feed. Pure.
312
- */
313
- export function normalizeReadinessEvidence(raw: unknown): ReadinessEvidence {
314
- if (!raw || typeof raw !== "object") {
315
- return { dualRun: null, targetHealthy: false, liveDigest: null };
316
- }
317
- const o = raw as Record<string, unknown>;
318
- return {
319
- dualRun: normalizeDualRunEvidence(o.dualRun ?? o),
320
- targetHealthy: o.targetHealthy === true,
321
- liveDigest: typeof o.liveDigest === "string" ? o.liveDigest : null,
322
- };
323
- }
324
-
325
- /** The state fields the readiness signals draw from `DomainState` (structural, no import). */
326
- export interface ReadinessDomainState {
327
- /** True when the last run is still awaiting its CI callback (`runInFlight`). */
328
- runInFlight: boolean;
329
- /** The captured prior-DNS-records pointer (`priorRecordsKey`), or null. */
330
- priorRecordsKey: string | null;
331
- /** When those prior records were captured (`priorCapturedAt`), or null. */
332
- priorCapturedAt: string | null;
333
- }
334
-
335
- /**
336
- * Assemble the {@link ApexReadinessSignals} the gate decides over from the three
337
- * live sources — the owner capability, the per-tenant domain state (revert +
338
- * in-flight), and the consumed evidence record — PURE so the endpoint's mapping is
339
- * unit-testable without KV/session I/O. The endpoint does the reads; this does the
340
- * mapping.
341
- */
342
- export function assembleApexReadinessSignals(input: {
343
- ownerCapable: boolean;
344
- state: ReadinessDomainState;
345
- evidence: ReadinessEvidence;
346
- }): ApexReadinessSignals {
347
- return {
348
- ownerCapable: input.ownerCapable,
349
- runInFlight: input.state.runInFlight,
350
- dualRun: input.evidence.dualRun,
351
- targetHealthy: input.evidence.targetHealthy,
352
- liveDigest: input.evidence.liveDigest,
353
- revert: { key: input.state.priorRecordsKey, capturedAt: input.state.priorCapturedAt },
354
- };
355
- }
356
-
357
- /** Short 12-char digest for legible details (full digest stays in the raw signal). */
358
- function short(digest: string | null): string {
359
- return digest ? digest.slice(0, 12) : "(none)";
360
- }
@@ -1,269 +0,0 @@
1
- /**
2
- * Pure logic for the admin Publish tab's real-time build/check streaming (SSE).
3
- *
4
- * The build endpoint (`POST /api/preview/build` with `Accept: text/event-stream`)
5
- * streams coarse lifecycle PHASES as it materializes a candidate — resolve head →
6
- * read+validate tenant files → publish the immutable version → run evidence checks —
7
- * then a terminal `result` event carrying the full outcome. This module is the tested
8
- * source of truth for:
9
- * - parsing the raw SSE byte-frames into events (partial-chunk safe);
10
- * - mapping a phase to its human label + rail position;
11
- * - reducing the event sequence into a render-ready progress state;
12
- * - assembling the "Copy for your AI agent" prompt from a build FAILURE.
13
- * The DOM wiring that drives the dialog from these is verified on a live preview, per
14
- * this codebase's client-island convention (see reviewReleaseWorkspace/controller.ts).
15
- */
16
-
17
- /** One decoded SSE event: its `event:` name and the concatenated `data:` payload. */
18
- export interface SseEvent {
19
- event: string;
20
- data: string;
21
- }
22
-
23
- /**
24
- * Parse an accumulated SSE text buffer into complete events, returning any trailing
25
- * INCOMPLETE frame as `rest` for the next chunk to prepend. Partial-chunk safe: a
26
- * frame is complete only once its blank-line terminator (`\n\n`) has arrived.
27
- * Lines with no field (`:` comments, stray blanks) are ignored; multiple `data:`
28
- * lines in one frame join with `\n` (the SSE spec). A frame with no fields yields
29
- * nothing.
30
- */
31
- export function parseSseChunk(buffer: string): { events: SseEvent[]; rest: string } {
32
- const normalized = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
33
- const events: SseEvent[] = [];
34
- const lastSep = normalized.lastIndexOf("\n\n");
35
- if (lastSep === -1) return { events, rest: normalized };
36
-
37
- const complete = normalized.slice(0, lastSep);
38
- const rest = normalized.slice(lastSep + 2);
39
-
40
- for (const frame of complete.split("\n\n")) {
41
- let eventName = "message";
42
- const dataLines: string[] = [];
43
- let sawField = false;
44
- for (const rawLine of frame.split("\n")) {
45
- if (!rawLine || rawLine.startsWith(":")) continue;
46
- const colon = rawLine.indexOf(":");
47
- const field = colon === -1 ? rawLine : rawLine.slice(0, colon);
48
- // Per spec a single leading space after the colon is stripped.
49
- let value = colon === -1 ? "" : rawLine.slice(colon + 1);
50
- if (value.startsWith(" ")) value = value.slice(1);
51
- if (field === "event") {
52
- eventName = value;
53
- sawField = true;
54
- } else if (field === "data") {
55
- dataLines.push(value);
56
- sawField = true;
57
- }
58
- }
59
- if (sawField) events.push({ event: eventName, data: dataLines.join("\n") });
60
- }
61
- return { events, rest };
62
- }
63
-
64
- /** The ordered build lifecycle stages the flight rail renders (mirrors the dialog). */
65
- export const BUILD_STAGES = ["resolve", "read", "publish", "check"] as const;
66
- export type BuildStage = (typeof BUILD_STAGES)[number];
67
-
68
- /** The `phase` values the server emits, in lifecycle order. */
69
- export type BuildPhase = "resolving" | "reading" | "publishing" | "checking";
70
-
71
- const PHASE_TO_STAGE: Record<BuildPhase, BuildStage> = {
72
- resolving: "resolve",
73
- reading: "read",
74
- publishing: "publish",
75
- checking: "check",
76
- };
77
-
78
- const PHASE_LABEL: Record<BuildPhase, string> = {
79
- resolving: "Locating the pull request's latest commit…",
80
- reading: "Reading and validating your store's files…",
81
- publishing: "Publishing this version's preview…",
82
- checking: "Running the checks on this version…",
83
- };
84
-
85
- const STAGE_LABEL: Record<BuildStage, string> = {
86
- resolve: "Locate commit",
87
- read: "Read & validate",
88
- publish: "Publish version",
89
- check: "Run checks",
90
- };
91
-
92
- export function buildPhaseLabel(phase: BuildPhase): string {
93
- return PHASE_LABEL[phase];
94
- }
95
-
96
- export function buildStageLabel(stage: BuildStage): string {
97
- return STAGE_LABEL[stage];
98
- }
99
-
100
- export function isBuildPhase(value: unknown): value is BuildPhase {
101
- return value === "resolving" || value === "reading" || value === "publishing" || value === "checking";
102
- }
103
-
104
- /** The terminal outcome the server reports on its `result` event. */
105
- export type BuildOutcome = "ok" | "blocked" | "failed";
106
-
107
- /** The full terminal payload of a build (mirrors the endpoint's JSON body). */
108
- export interface BuildResult {
109
- ok: boolean;
110
- outcome: BuildOutcome;
111
- tenantId?: string | null;
112
- changeId?: string | null;
113
- headSha?: string | null;
114
- prNumber?: number | null;
115
- previewUrl?: string | null;
116
- errors?: string[];
117
- error?: string | null;
118
- findings?: BuildFinding[];
119
- }
120
-
121
- export interface BuildFinding {
122
- id?: string;
123
- label?: string;
124
- detail?: string;
125
- disposition?: string;
126
- }
127
-
128
- /** Render-ready progress the dialog reads. `stageState` is a per-stage rail map. */
129
- export interface BuildProgress {
130
- phase: BuildPhase | null;
131
- liveLabel: string;
132
- stageState: Record<BuildStage, "pending" | "active" | "done" | "fail">;
133
- terminal: boolean;
134
- result: BuildResult | null;
135
- }
136
-
137
- export function initialBuildProgress(): BuildProgress {
138
- return {
139
- phase: null,
140
- liveLabel: "Starting…",
141
- stageState: { resolve: "pending", read: "pending", publish: "pending", check: "pending" },
142
- terminal: false,
143
- result: null,
144
- };
145
- }
146
-
147
- /**
148
- * Advance the flight rail to `stage`: everything before it is `done`, it is `active`,
149
- * everything after stays `pending`. A `fail` mark is never un-done. Pure.
150
- */
151
- function railTo(
152
- prev: Record<BuildStage, "pending" | "active" | "done" | "fail">,
153
- stage: BuildStage,
154
- ): Record<BuildStage, "pending" | "active" | "done" | "fail"> {
155
- const idx = BUILD_STAGES.indexOf(stage);
156
- const next = { ...prev };
157
- BUILD_STAGES.forEach((s, i) => {
158
- if (next[s] === "fail") return;
159
- next[s] = i < idx ? "done" : i === idx ? "active" : "pending";
160
- });
161
- return next;
162
- }
163
-
164
- /**
165
- * Fold one SSE event into the progress state. A `phase` event advances the rail + live
166
- * label; a `result` event is terminal (all reached stages `done` on success, the active
167
- * stage `fail` on a blocked/failed outcome). Unknown events are ignored (state
168
- * unchanged). Pure — the dialog calls this per event and re-renders from the result.
169
- */
170
- export function reduceBuildProgress(prev: BuildProgress, ev: SseEvent): BuildProgress {
171
- if (ev.event === "phase") {
172
- const parsed = safeParse(ev.data);
173
- const phase = parsed && (parsed as { phase?: unknown }).phase;
174
- if (!isBuildPhase(phase)) return prev;
175
- const stage = PHASE_TO_STAGE[phase];
176
- const label =
177
- (parsed && typeof (parsed as { label?: unknown }).label === "string"
178
- ? (parsed as { label: string }).label
179
- : "") || buildPhaseLabel(phase);
180
- return {
181
- ...prev,
182
- phase,
183
- liveLabel: label,
184
- stageState: railTo(prev.stageState, stage),
185
- };
186
- }
187
-
188
- if (ev.event === "result") {
189
- const parsed = (safeParse(ev.data) as BuildResult | null) ?? {
190
- ok: false,
191
- outcome: "failed",
192
- errors: ["build ended without a result"],
193
- };
194
- const result: BuildResult = {
195
- ...parsed,
196
- outcome: parsed.outcome ?? (parsed.ok ? "ok" : "failed"),
197
- };
198
- let stageState = prev.stageState;
199
- if (result.ok) {
200
- stageState = { resolve: "done", read: "done", publish: "done", check: "done" };
201
- } else {
202
- // Fail the stage that was active (the phase we got furthest into); if none was
203
- // active yet, fail the first stage so the rail shows an honest stop point.
204
- const active = BUILD_STAGES.find((s) => prev.stageState[s] === "active") ?? BUILD_STAGES[0];
205
- stageState = { ...prev.stageState, [active]: "fail" };
206
- }
207
- return {
208
- ...prev,
209
- liveLabel: result.ok ? "Done." : "Stopped.",
210
- stageState,
211
- terminal: true,
212
- result,
213
- };
214
- }
215
-
216
- return prev;
217
- }
218
-
219
- /** The human-readable failure detail (the honest reason), joined from errors/findings. */
220
- export function buildFailureDetail(result: BuildResult): string {
221
- const parts: string[] = [];
222
- for (const e of result.errors ?? []) if (e && e.trim()) parts.push(e.trim());
223
- if (result.error && result.error.trim()) parts.push(result.error.trim());
224
- for (const f of result.findings ?? []) {
225
- const line = [f.label || f.id, f.detail].filter(Boolean).join(": ");
226
- if (line) parts.push((f.disposition === "blocking-safety" ? "[blocking] " : "") + line);
227
- }
228
- return parts.join("\n");
229
- }
230
-
231
- /**
232
- * Assemble the "Copy for your AI agent" prompt from a build FAILURE — the honest,
233
- * paste-ready brief an operator hands to a coding agent to diagnose why the preview
234
- * build/checks failed. Mirrors the merge-conflict copy pattern (a framed ask + the
235
- * system's own words), never a bare error dump.
236
- */
237
- export function assembleBuildFailureCopy(input: {
238
- tenant: string;
239
- prNumber?: number | null;
240
- changeId?: string | null;
241
- result: BuildResult;
242
- }): string {
243
- const { tenant, prNumber, changeId, result } = input;
244
- const pr = prNumber != null ? `PR #${prNumber}` : changeId ? `change ${changeId}` : "my open candidate PR";
245
- const stage = result.outcome === "blocked" ? "was blocked by a check" : "failed to build";
246
- const detail = buildFailureDetail(result) || "(no detail was reported)";
247
- return [
248
- `In my ${tenant} store, the preview build for ${pr} ${stage}. ` +
249
- `Diagnose why from the detail below, then tell me the exact file(s) to change and the fix.`,
250
- "",
251
- "What the build reported:",
252
- detail,
253
- "",
254
- result.headSha ? `Head commit: ${result.headSha}` : "",
255
- "Context: this is a Token of Trust storefront tenant repo — customization files are " +
256
- "mapped to artifacts and validated against the tenant-editable manifest before the " +
257
- "version is published. A blocking finding or a validation error stops the build.",
258
- ]
259
- .filter((l) => l !== "")
260
- .join("\n");
261
- }
262
-
263
- function safeParse(text: string): unknown {
264
- try {
265
- return JSON.parse(text);
266
- } catch {
267
- return null;
268
- }
269
- }