@tokenoftrust/storefront-runner 2.0.1 → 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 (120) hide show
  1. package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +19 -1
  2. package/apps/storefront/dev-plugins/tenant-validate-on-save.mjs +78 -0
  3. package/apps/storefront/src/middleware/index.ts +17 -1
  4. package/package.json +1 -1
  5. package/scripts/lib/tenant-dirs.mjs +80 -0
  6. package/scripts/tenant/lint-embeds.mjs +180 -0
  7. package/scripts/tenant/materialize-tenant-content.mjs +533 -0
  8. package/scripts/tenant/validate.mjs +837 -0
  9. package/apps/storefront/scripts/build-v84-clean-clone-standalone.mjs +0 -68
  10. package/apps/storefront/scripts/deny-v84-clean-clone-standalone-capabilities.mjs +0 -111
  11. package/apps/storefront/scripts/deny-v84-dogfood-capabilities.mjs +0 -2
  12. package/apps/storefront/scripts/deny-v84-lifecycle-capabilities.mjs +0 -247
  13. package/apps/storefront/scripts/dogfood-v84-nonportable-routes.ts +0 -73
  14. package/apps/storefront/scripts/fixed-v84-native-install-producer.ts +0 -79
  15. package/apps/storefront/scripts/generated/v84-clean-clone-replay.bundle.mjs +0 -10982
  16. package/apps/storefront/scripts/import-v84-through-ws2.ts +0 -38
  17. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling-dependency.ts +0 -2
  18. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling.ts +0 -4
  19. package/apps/storefront/scripts/install-v84-native-through-ws1.ts +0 -185
  20. package/apps/storefront/scripts/prove-clone-isolation.ts +0 -506
  21. package/apps/storefront/scripts/replay-v84-clean-clone-pass.ts +0 -148
  22. package/apps/storefront/scripts/replay-v84-clean-clone-standalone.ts +0 -35
  23. package/apps/storefront/scripts/replay-v84-clean-clone.ts +0 -214
  24. package/apps/storefront/scripts/run-fixed-v84-native-install-producer.mjs +0 -18
  25. package/apps/storefront/scripts/run-v84-clone-isolation-proof.mjs +0 -18
  26. package/apps/storefront/scripts/run-v84-clone-lifecycle-proof.mjs +0 -18
  27. package/apps/storefront/scripts/run-v84-import-through-ws2-proof.mjs +0 -17
  28. package/apps/storefront/scripts/run-v84-nonportable-route-dogfood.mjs +0 -15
  29. package/apps/storefront/scripts/sanitize-v84-proof-environment.mjs +0 -21
  30. package/apps/storefront/scripts/v84CleanCloneGraphPin.ts +0 -42
  31. package/apps/storefront/scripts/v84CleanCloneSourceGraph.ts +0 -145
  32. package/apps/storefront/scripts/validate-v84-clone-lifecycle.ts +0 -18
  33. package/apps/storefront/src/components/admin/AdminAIWorkflowsTab.astro +0 -531
  34. package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +0 -878
  35. package/apps/storefront/src/components/admin/AdminBlogTab.astro +0 -1435
  36. package/apps/storefront/src/components/admin/AdminCheckoutSettingsPanel.astro +0 -226
  37. package/apps/storefront/src/components/admin/AdminCustomersTab.astro +0 -593
  38. package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +0 -465
  39. package/apps/storefront/src/components/admin/AdminIntegrationsTab.astro +0 -105
  40. package/apps/storefront/src/components/admin/AdminProductsTab.astro +0 -901
  41. package/apps/storefront/src/components/admin/AdminPublishTab.astro +0 -2978
  42. package/apps/storefront/src/components/admin/AdminReportingTab.astro +0 -435
  43. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +0 -782
  44. package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +0 -611
  45. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +0 -1223
  46. package/apps/storefront/src/lib/dev/cockpitStore.ts +0 -57
  47. package/apps/storefront/src/lib/publish/adminPublishTab.ts +0 -4625
  48. package/apps/storefront/src/lib/publish/apex-readiness.ts +0 -360
  49. package/apps/storefront/src/lib/publish/buildStream.ts +0 -269
  50. package/apps/storefront/src/lib/publish/collapsePersistence.ts +0 -72
  51. package/apps/storefront/src/lib/publish/dispatchHealth.ts +0 -267
  52. package/apps/storefront/src/lib/publish/domainState.ts +0 -360
  53. package/apps/storefront/src/lib/publish/lifecycleState.ts +0 -369
  54. package/apps/storefront/src/lib/publish/liveReleaseSummary.ts +0 -29
  55. package/apps/storefront/src/lib/publish/mergeDoctorOnFailure.ts +0 -78
  56. package/apps/storefront/src/lib/publish/mergeHealthBanner.ts +0 -62
  57. package/apps/storefront/src/lib/publish/mergeRowFindings.ts +0 -124
  58. package/apps/storefront/src/lib/publish/mergeSupportEscalation.ts +0 -75
  59. package/apps/storefront/src/lib/publish/panelPolling.ts +0 -78
  60. package/apps/storefront/src/lib/publish/publish-guard.ts +0 -88
  61. package/apps/storefront/src/lib/publish/publishFormat.ts +0 -165
  62. package/apps/storefront/src/lib/publish/publishHero.ts +0 -177
  63. package/apps/storefront/src/lib/publish/reviewRelease/changeState.ts +0 -66
  64. package/apps/storefront/src/lib/publish/reviewRelease/copy.ts +0 -141
  65. package/apps/storefront/src/lib/publish/reviewRelease/index.ts +0 -41
  66. package/apps/storefront/src/lib/publish/reviewRelease/permissions.ts +0 -47
  67. package/apps/storefront/src/lib/publish/reviewRelease/releaseState.ts +0 -42
  68. package/apps/storefront/src/lib/publish/reviewRelease/types.ts +0 -59
  69. package/apps/storefront/src/lib/publish/reviewRelease/viewModel.ts +0 -440
  70. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/actions.ts +0 -161
  71. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/controller.ts +0 -325
  72. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/render.ts +0 -241
  73. package/apps/storefront/src/lib/publish/shipWorkspace.ts +0 -439
  74. package/apps/storefront/src/lib/publish/sseRoute.ts +0 -119
  75. package/apps/storefront/src/lib/publish/stageBundleUpload.ts +0 -215
  76. package/apps/storefront/src/lib/publish/streamLogConsole.ts +0 -142
  77. package/apps/storefront/src/lib/publish/versionNumber.ts +0 -52
  78. package/apps/storefront/src/pages/admin/ops-timeline.astro +0 -420
  79. package/apps/storefront/src/pages/admin/review-release.astro +0 -265
  80. package/apps/storefront/src/pages/admin/select.astro +0 -96
  81. package/apps/storefront/src/pages/admin.astro +0 -568
  82. package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +0 -86
  83. package/apps/storefront/src/pages/api/apps/admin/gtm-migration-install.ts +0 -174
  84. package/apps/storefront/src/pages/api/apps/admin/gtm-migration.ts +0 -96
  85. package/apps/storefront/src/pages/api/apps/admin/health.ts +0 -44
  86. package/apps/storefront/src/pages/api/apps/admin/install.ts +0 -125
  87. package/apps/storefront/src/pages/api/apps/admin/integration-lifecycle.ts +0 -118
  88. package/apps/storefront/src/pages/api/apps/admin/integrations.ts +0 -89
  89. package/apps/storefront/src/pages/api/apps/admin/list.ts +0 -26
  90. package/apps/storefront/src/pages/api/apps/admin/platform-adapters.ts +0 -85
  91. package/apps/storefront/src/pages/api/apps/admin/resume.ts +0 -79
  92. package/apps/storefront/src/pages/api/apps/admin/suspend.ts +0 -80
  93. package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +0 -98
  94. package/apps/storefront/src/pages/api/apps/admin/update.ts +0 -126
  95. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +0 -70
  96. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +0 -51
  97. package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +0 -255
  98. package/apps/storefront/src/pages/api/apps/oauth/token.ts +0 -87
  99. package/apps/storefront/src/pages/api/apps/v1/attribution.ts +0 -162
  100. package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +0 -39
  101. package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +0 -49
  102. package/apps/storefront/src/pages/api/apps/v1/health.ts +0 -32
  103. package/apps/storefront/src/pages/api/apps/v1/inventory.ts +0 -59
  104. package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +0 -48
  105. package/apps/storefront/src/pages/api/apps/v1/orders.ts +0 -73
  106. package/apps/storefront/src/pages/api/apps/v1/reports.ts +0 -21
  107. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +0 -74
  108. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +0 -47
  109. package/apps/storefront/src/pages/api/cache-purge.ts +0 -55
  110. package/apps/storefront/src/pages/api/compliance/tamper.ts +0 -88
  111. package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +0 -154
  112. package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +0 -120
  113. package/apps/storefront/src/pages/api/integrations/v1/[operation].ts +0 -108
  114. package/apps/storefront/src/pages/api/publish/stage-bundle.ts +0 -70
  115. package/apps/storefront/src/pages/api/test/dev-session.ts +0 -133
  116. package/apps/storefront/src/pages/cockpit.astro +0 -1548
  117. package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +0 -119
  118. package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +0 -105
  119. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +0 -177
  120. package/apps/storefront/src/pages/dashboard/index.astro +0 -160
@@ -1,439 +0,0 @@
1
- /**
2
- * Ship operations workspace — the PURE composer behind the admin
3
- * Publish tab's operations surface that sits ALONGSIDE the go-live core
4
- * (AdminPublishTab.astro's change queue + accept/reject + dispatch status).
5
- *
6
- * 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:
10
- *
11
- * 1. DIFF VIEW — for one pending candidate, the artifact-level diff of what
12
- * changed (its rendered immutable version vs the current live version),
13
- * tied to the exact tenant Gitea head SHA, with the immutable
14
- * `/<tenant>/preview/rev/<sha>/` link and the evidence AUTHORITY
15
- * (authoritative / stale / missing) — never a diff a reviewer would mistake
16
- * for the head when a last-good version actually renders.
17
- * 2. VERSIONS — the tenant's published-version / promotion history
18
- * (`listPromotions`, promotion_status), each row carrying the tenant
19
- * Gitea SHA, its promotion time, a
20
- * `/rev/<sha>/` link, and — marking the current live one.
21
- * 3. ROLLBACK — pick a prior version and roll back THROUGH the one orchestrator
22
- * (`/api/changes/rollback` → `rollbackCandidateAtomic` /
23
- * `staticRollbackOrchestrator`), rendering only the HONEST terminal state
24
- * (`rolled_back` when VERIFIED; `publish_pending` in flight; `publish_failed`
25
- * / `blocked` fail-closed) — never a false "rolled back".
26
- *
27
- * COMPOSES, never rebuilds:
28
- * - the immutable link is `reviewDashboardRevisionHref` verbatim;
29
- * - the authority is `candidateEvidenceAuthority` /
30
- * `classifyCandidateForRender` verbatim;
31
- * - the versions come from the SAME `KVVersionStore.listPromotions` the
32
- * "one step back" rollback resolution reads;
33
- * - the rollback ACTION is the existing `/api/changes/rollback` seam — this
34
- * module only INTERPRETS its honest response, it never re-decides a ship.
35
- *
36
- * PURE (no I/O): the endpoints inject already-read KV data, so every honest-state
37
- * decision is unit-tested without a Worker/KV — mirroring the reviewDashboard.ts /
38
- * candidatePreviewState.ts split.
39
- *
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.
47
- */
48
- import { reviewDashboardRevisionHref } from "@/lib/preview/reviewDashboard";
49
- import { assignVersionNumbers } from "@/lib/publish/versionNumber";
50
- import {
51
- candidateEvidenceAuthority,
52
- classifyCandidateForRender,
53
- defaultBlockedReason,
54
- type CandidateEvidenceAuthority,
55
- } from "@/lib/preview/candidatePreviewState";
56
- import type { ReviewEnvironment, PromotionEvent } from "@tot/public-runtime";
57
-
58
- // ── Versions / promotion history ───────────────────────────────────────────
59
-
60
- /** The plain-language "what shipped in this version" model (same shape the staged
61
- * release summary uses — a name + a lead sentence + one highlight per included change). */
62
- export interface VersionReleaseSummary {
63
- /** The human-readable release name (see `deriveReleaseName`), or null. */
64
- name: string | null;
65
- lead: string;
66
- highlights: { title: string; detail: string }[];
67
- }
68
-
69
- /** One change that shipped in a published version — its PR summary, for the
70
- * version's openable "what shipped" list. Fields degrade to null/`PR #N` when
71
- * candidate_summary didn't cover the PR (best-effort enrichment). */
72
- export interface WorkspaceVersionChange {
73
- changeId: string;
74
- prNumber: number | null;
75
- headSha: string;
76
- title: string | null;
77
- description: string | null;
78
- fileCount: number | null;
79
- linesAdded: number | null;
80
- linesRemoved: number | null;
81
- }
82
-
83
- /** Per-version release enrichment, keyed by versionId (== the promoted aggregate sha). */
84
- export interface VersionReleaseInfo {
85
- releaseSummary: VersionReleaseSummary | null;
86
- changes: WorkspaceVersionChange[];
87
- }
88
-
89
- /** One published version in the tenant's promotion history (promotion_status). */
90
- export interface WorkspaceVersion {
91
- /** The tenant Gitea SHA the version serves — the content identity. */
92
- versionId: string;
93
- /**
94
- * The merchant-facing monotonic version number ("v42") — derived from first
95
- * promotion order (see `versionNumber.ts`), shared with the "Live version"
96
- * metric. The human label; the SHA is demoted to a technical disclosure.
97
- */
98
- number: number | null;
99
- /** How the version was published (a Gitea commit, or a content hash). */
100
- source: "commit" | "hash" | null;
101
- /** ISO time the version was PROMOTED onto `live` (the promotion event). */
102
- promotedAt: string;
103
- /** ISO time the version was first published (from its site-version manifest). */
104
- createdAt: string | null;
105
- /** Artifact count in the version manifest, or `null` when the manifest is absent. */
106
- artifactCount: number | null;
107
- /**
108
- * The content-addressed `treeDigest`, when the console can honestly know it —
109
- * i.e. ONLY the current live version of a static tenant (read back from
110
- * `live/manifest.json`). `null` for every other row: a prior static version's
111
- * digest lives only in the S3 archive the Worker can't enumerate. Never fabricated.
112
- */
113
- digest: string | null;
114
- /** The immutable `/<tenant>/preview/rev/<sha>/` deep-link, or `null` without a SHA. */
115
- revHref: string | null;
116
- /** The version currently serving live (the one you never roll back TO). */
117
- isCurrent: boolean;
118
- /**
119
- * The plain-language "what shipped in this version" summary, composed from the
120
- * release receipt's included PRs + their candidate_summary. `null` when no
121
- * receipt links this version to a PR set, or none carried summary prose.
122
- */
123
- releaseSummary: VersionReleaseSummary | null;
124
- /**
125
- * The PRs that shipped in this version (from its release receipt), each with its
126
- * human summary — the version's openable "what shipped" list. Empty when no
127
- * receipt links this version to a PR set.
128
- */
129
- changes: WorkspaceVersionChange[];
130
- /** How many changes shipped in this version, or `null` when unknown (no receipt). */
131
- changeCount: number | null;
132
- }
133
-
134
- /**
135
- * How a rollback is honestly driven for this tenant. `control-plane` (hosted) and
136
- * `pointer` (static-publish) both roll back one-click via the instant KV `live`
137
- * pointer flip; they render identical instant "Roll back" copy, differing only in
138
- * whether the static digest column is shown.
139
- */
140
- export type RollbackMode = "control-plane" | "pointer";
141
-
142
- /** The versions-panel view model. */
143
- export interface VersionsView {
144
- /** The current live version + (static) the digest `live/` actually serves. */
145
- current: { versionId: string | null; number: number | null; digest: string | null };
146
- /** Promotion history, newest first, current marked. */
147
- versions: WorkspaceVersion[];
148
- /** Whether the tenant publishes its public site to an external static target. */
149
- staticPublish: boolean;
150
- /**
151
- * `control-plane` (hosted) and `pointer` (static-publish) both roll back
152
- * one-click via `targetVersionId` (instant KV pointer flip) and render identical
153
- * instant "Roll back" copy.
154
- */
155
- rollbackMode: RollbackMode;
156
- }
157
-
158
- /** A single promoted version enriched with the manifest facts the panel shows. */
159
- export interface VersionManifestFacts {
160
- source: "commit" | "hash" | null;
161
- createdAt: string | null;
162
- artifactCount: number | null;
163
- }
164
-
165
- export interface BuildVersionsViewInput {
166
- tenantId: string;
167
- /** Live-promotion history, newest first (`KVVersionStore.listPromotions`). */
168
- promotions: PromotionEvent[];
169
- /** The current live version id (the live-channel pointer), or `null`. */
170
- currentVersionId: string | null;
171
- /** The digest `live/` serves — static tenants only; `null` otherwise. */
172
- currentDigest: string | null;
173
- /** Per-version manifest facts, keyed by versionId (absent ⇒ unknown facts). */
174
- manifests: Map<string, VersionManifestFacts>;
175
- /**
176
- * Per-version release enrichment, keyed by versionId (== the promoted aggregate
177
- * sha on the release receipt). Absent ⇒ a version with no linked PR set (an
178
- * honest empty release summary + no changes list — never fabricated).
179
- */
180
- releases?: Map<string, VersionReleaseInfo>;
181
- staticPublish: boolean;
182
- }
183
-
184
- /**
185
- * Assemble the versions panel from the promotion history + per-version manifest
186
- * facts + the current-live pointer. Pure. The current live row is marked; the
187
- * digest is populated ONLY for the current live of a static tenant (never faked
188
- * for priors). `revHref` is the immutable link.
189
- */
190
- export function buildVersionsView(input: BuildVersionsViewInput): VersionsView {
191
- const {
192
- tenantId,
193
- promotions,
194
- currentVersionId,
195
- currentDigest,
196
- manifests,
197
- releases,
198
- staticPublish,
199
- } = input;
200
-
201
- // The merchant-facing "v42" counter, derived from first-promotion order — the
202
- // SAME derivation `resolveLiveVersion` feeds the "Live version" metric, so the
203
- // panel and the metric always agree on a version's number.
204
- const numbers = assignVersionNumbers(promotions);
205
-
206
- const versions: WorkspaceVersion[] = promotions.map((p) => {
207
- const facts = manifests.get(p.versionId);
208
- const release = releases?.get(p.versionId);
209
- const isCurrent = currentVersionId != null && p.versionId === currentVersionId;
210
- return {
211
- versionId: p.versionId,
212
- number: numbers.get(p.versionId) ?? null,
213
- source: facts?.source ?? null,
214
- promotedAt: p.at,
215
- createdAt: facts?.createdAt ?? null,
216
- artifactCount: facts?.artifactCount ?? null,
217
- // Honest: a digest is known only for the current live of a static tenant.
218
- digest: isCurrent && staticPublish ? currentDigest : null,
219
- revHref: reviewDashboardRevisionHref(tenantId, p.versionId),
220
- isCurrent,
221
- releaseSummary: release?.releaseSummary ?? null,
222
- changes: release?.changes ?? [],
223
- changeCount: release ? release.changes.length : null,
224
- };
225
- });
226
-
227
- return {
228
- current: {
229
- versionId: currentVersionId,
230
- number: currentVersionId != null ? numbers.get(currentVersionId) ?? null : null,
231
- digest: staticPublish ? currentDigest : null,
232
- },
233
- versions,
234
- staticPublish,
235
- rollbackMode: staticPublish ? "pointer" : "control-plane",
236
- };
237
- }
238
-
239
- // ── Diff view ──────────────────────────────────────────────────────────────
240
-
241
- export type DiffChangeKind = "added" | "removed" | "changed";
242
-
243
- /** One artifact path that differs between the candidate and the current live version. */
244
- export interface DiffEntry {
245
- path: string;
246
- change: DiffChangeKind;
247
- }
248
-
249
- /** The diff-view model for one pending candidate. */
250
- export interface CandidateDiffView {
251
- changeId: string;
252
- /** The candidate's exact head SHA — the reconcile TARGET (tenant Gitea sha). */
253
- headSha: string | null;
254
- /** The immutable version actually rendered + diffed — the reconcile ACTUAL. */
255
- versionId: string | null;
256
- /** The immutable link to the exact reviewed SHA, or `null` without a head SHA. */
257
- revHref: string | null;
258
- /** Evidence authority: authoritative (tied to head) / stale (last-good) / missing. */
259
- authority: CandidateEvidenceAuthority;
260
- /** The artifact-level diff (candidate rendered version vs current live), sorted by path. */
261
- files: DiffEntry[];
262
- /** True when the candidate has a renderable version to diff (the `ready` classification). */
263
- renderable: boolean;
264
- /** When NOT renderable, the honest reason (names why) — never a fake empty diff. */
265
- blockedReason: string | null;
266
- }
267
-
268
- export interface BuildCandidateDiffInput {
269
- tenantId: string;
270
- env: ReviewEnvironment;
271
- /** The candidate rendered version's artifacts (path → contentHash), or `null`. */
272
- candidateArtifacts: Record<string, string> | null;
273
- /** The current live version's artifacts (path → contentHash), or `null` (nothing live yet). */
274
- liveArtifacts: Record<string, string> | null;
275
- }
276
-
277
- /**
278
- * Diff a candidate's rendered version against the current live version, at the
279
- * artifact level (each path carries a content hash in its site-version manifest).
280
- * Composes `classifyCandidateForRender` (only a `ready` candidate has a
281
- * version to diff — a blocked/queued one yields an HONEST `blockedReason`, never a
282
- * fabricated empty diff) and `candidateEvidenceAuthority` (so a diff of a last-good
283
- * version that LAGS the head is flagged `stale`, not passed off as the head).
284
- * Pure.
285
- */
286
- export function buildCandidateDiff(input: BuildCandidateDiffInput): CandidateDiffView {
287
- const { tenantId, env, candidateArtifacts, liveArtifacts } = input;
288
- const classification = classifyCandidateForRender(env);
289
- const authority = candidateEvidenceAuthority(env);
290
- const revHref = reviewDashboardRevisionHref(tenantId, env.headSha);
291
-
292
- if (classification.kind !== "ready") {
293
- const reason =
294
- classification.kind === "blocked"
295
- ? classification.reason
296
- : defaultBlockedReason(env.status);
297
- return {
298
- changeId: env.changeId,
299
- headSha: env.headSha,
300
- versionId: null,
301
- revHref,
302
- authority,
303
- files: [],
304
- renderable: false,
305
- blockedReason: reason,
306
- };
307
- }
308
-
309
- const files = diffArtifacts(candidateArtifacts ?? {}, liveArtifacts ?? {});
310
- return {
311
- changeId: env.changeId,
312
- headSha: env.headSha,
313
- versionId: classification.versionId,
314
- revHref,
315
- authority,
316
- files,
317
- renderable: true,
318
- blockedReason: null,
319
- };
320
- }
321
-
322
- /** Artifact-level diff: paths added / removed / content-changed, sorted by path. */
323
- export function diffArtifacts(
324
- candidate: Record<string, string>,
325
- live: Record<string, string>,
326
- ): DiffEntry[] {
327
- const entries: DiffEntry[] = [];
328
- for (const [path, hash] of Object.entries(candidate)) {
329
- if (!(path in live)) entries.push({ path, change: "added" });
330
- else if (live[path] !== hash) entries.push({ path, change: "changed" });
331
- }
332
- for (const path of Object.keys(live)) {
333
- if (!(path in candidate)) entries.push({ path, change: "removed" });
334
- }
335
- return entries.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
336
- }
337
-
338
- // ── Rollback response interpretation ─────────────────────────────────────────
339
-
340
- /** The normalized honest terminal state of a rollback, for the surface to render. */
341
- export type RollbackState =
342
- | "rolled_back" // VERIFIED live on the prior version — the ONLY "rolled back" claim
343
- | "publish_pending" // restored + dispatched, not yet verified — NOT "rolled back"
344
- | "publish_failed" // fail-closed: unwired / dispatch failed / verification failed
345
- | "blocked" // a gate refused (capability / no prior / race) — nothing moved
346
- | "error"; // transport / unexpected shape
347
-
348
- export interface RollbackOutcome {
349
- state: RollbackState;
350
- /** True ONLY for `rolled_back` (the target is verified to serve the prior version). */
351
- rolledBack: boolean;
352
- /** The version restored TO, when known. */
353
- toVersionId: string | null;
354
- /** A human-legible message for the surface. */
355
- message: string;
356
- }
357
-
358
- /**
359
- * Interpret the `/api/changes/rollback` response into ONE honest terminal state,
360
- * for BOTH planes without the surface having to know which it hit:
361
- * - a HOSTED (control-plane) rollback returns `{ rolledBack: true, toVersionId }`
362
- * on success (the pointer flip IS the rollback) or `{ error, reason }` on refusal;
363
- * - a STATIC rollback returns the orchestrator's honest `{ state, ... }`.
364
- * The rule the surface depends on: `rolledBack` is true ONLY for a verified
365
- * `rolled_back` — a `publish_pending` is NEVER rendered as rolled back.
366
- * Pure.
367
- */
368
- export function interpretRollbackResponse(
369
- httpOk: boolean,
370
- body: {
371
- state?: unknown;
372
- rolledBack?: unknown;
373
- toVersionId?: unknown;
374
- message?: unknown;
375
- error?: unknown;
376
- reason?: unknown;
377
- },
378
- ): RollbackOutcome {
379
- const toVersionId = typeof body.toVersionId === "string" ? body.toVersionId : null;
380
-
381
- // Static plane: the orchestrator's explicit honest state wins.
382
- if (typeof body.state === "string") {
383
- const state = normalizeRollbackState(body.state);
384
- const message =
385
- typeof body.message === "string" && body.message
386
- ? body.message
387
- : typeof body.error === "string" && body.error
388
- ? body.error
389
- : rollbackStateFallbackMessage(state);
390
- return { state, rolledBack: state === "rolled_back", toVersionId, message };
391
- }
392
-
393
- // Control-plane: a bare success flag, or a typed refusal.
394
- if (httpOk && body.rolledBack === true) {
395
- return {
396
- state: "rolled_back",
397
- rolledBack: true,
398
- toVersionId,
399
- message: toVersionId ? `Rolled back live to ${toVersionId}.` : "Rolled back live.",
400
- };
401
- }
402
-
403
- const reason = typeof body.reason === "string" ? body.reason : null;
404
- const message =
405
- typeof body.error === "string" && body.error
406
- ? body.error + (reason ? ` (${reason})` : "")
407
- : "Rollback failed.";
408
- // A control-plane refusal is a gate refusal (capability / no prior / conflict) —
409
- // nothing moved; treat as blocked (never a false rolled-back).
410
- return { state: "blocked", rolledBack: false, toVersionId, message };
411
- }
412
-
413
- function normalizeRollbackState(raw: string): RollbackState {
414
- switch (raw) {
415
- case "rolled_back":
416
- case "publish_pending":
417
- case "publish_failed":
418
- case "blocked":
419
- return raw;
420
- default:
421
- return "error";
422
- }
423
- }
424
-
425
- function rollbackStateFallbackMessage(state: RollbackState): string {
426
- switch (state) {
427
- case "rolled_back":
428
- return "Rolled back live.";
429
- case "publish_pending":
430
- return "Restored the pointer and dispatched the re-publish — not yet verified live. Refresh to re-check.";
431
- case "publish_failed":
432
- return "Rollback failed — the publish path is unwired or verification failed. Nothing false went live.";
433
- case "blocked":
434
- return "Rollback was refused by a gate — nothing changed.";
435
- case "error":
436
- default:
437
- return "Rollback returned an unexpected response.";
438
- }
439
- }
@@ -1,119 +0,0 @@
1
- /**
2
- * The shared "buffer-or-stream on `Accept`" plumbing for a long-running admin
3
- * route. ONE emit-driven work function serves BOTH transports:
4
- *
5
- * - a non-streaming caller (the CLI, any `Accept: application/json` client) gets
6
- * the buffered terminal JSON body + status the work resolved — `emit` is a no-op;
7
- * - a streaming caller (`Accept: text/event-stream`, the admin Publish tab) gets
8
- * each `emit(event, data)` as an SSE frame AS IT HAPPENS, then a terminal
9
- * `result` event carrying the SAME payload the JSON body would have, then close.
10
- *
11
- * The work runs ONCE either way; the terminal payload + status are IDENTICAL. Only
12
- * the delivery differs. This is the generalized form of the pattern
13
- * `POST /api/preview/build` first shipped inline — extracted here so every
14
- * long-running route (build / integrate / rebuild / ship) streams the same way
15
- * without re-implementing the ReadableStream/encoder/close dance.
16
- */
17
-
18
- /**
19
- * A buffered JSON Response — the non-streaming terminal body. Inlined (not imported
20
- * from the change/grants controlplane) so this generic transport helper stays in the
21
- * `lib/publish` render layer, respecting the renderer-no-controlplane boundary.
22
- */
23
- function jsonResponse(body: unknown, status = 200): Response {
24
- return new Response(JSON.stringify(body), {
25
- status,
26
- headers: { "content-type": "application/json", "cache-control": "no-store" },
27
- });
28
- }
29
-
30
- /** Emit one SSE event. A no-op in buffered (JSON) mode. */
31
- export type SseEmit = (event: string, data: Record<string, unknown>) => void;
32
-
33
- /** The terminal outcome of the work: the HTTP status + the JSON/`result` payload. */
34
- export interface StreamWorkResult {
35
- status: number;
36
- payload: Record<string, unknown>;
37
- }
38
-
39
- /** True when the caller asked for an SSE stream (`Accept: text/event-stream`). */
40
- export function wantsEventStream(request: Request): boolean {
41
- return (request.headers.get("accept") || "").includes("text/event-stream");
42
- }
43
-
44
- /**
45
- * Run `work` and deliver its terminal payload as either a buffered JSON Response
46
- * (default) or a streamed SSE Response (when the caller sent
47
- * `Accept: text/event-stream`). In stream mode each `emit` becomes an
48
- * `event: <name>\ndata: <json>\n\n` frame; the terminal payload is sent as a
49
- * `result` event and the stream closes. `onError` maps an UNEXPECTED throw (the
50
- * work is expected to be fail-safe and return its own error payload) to a terminal
51
- * result — in JSON mode it becomes the response body+status, in SSE mode the
52
- * `result` event.
53
- */
54
- export async function respondBufferedOrStreamed(
55
- request: Request,
56
- work: (emit: SseEmit) => Promise<StreamWorkResult>,
57
- opts?: { onError?: (error: unknown) => StreamWorkResult },
58
- ): Promise<Response> {
59
- const onError =
60
- opts?.onError ??
61
- ((error: unknown): StreamWorkResult => ({
62
- status: 500,
63
- payload: {
64
- ok: false,
65
- outcome: "failed",
66
- errors: [`operation failed: ${error instanceof Error ? error.message : String(error)}`],
67
- },
68
- }));
69
-
70
- // Buffered JSON mode — the CLI / any non-streaming caller. `emit` is a no-op; the
71
- // work's own status + payload are returned verbatim (same contract as before).
72
- if (!wantsEventStream(request)) {
73
- try {
74
- const { status, payload } = await work(() => {});
75
- return jsonResponse(payload, status);
76
- } catch (error) {
77
- const { status, payload } = onError(error);
78
- return jsonResponse(payload, status);
79
- }
80
- }
81
-
82
- // SSE mode — stream each phase as it happens, then a terminal `result` event
83
- // carrying the SAME payload the JSON body would have, then close. Workers stream a
84
- // ReadableStream body natively — no infra.
85
- const encoder = new TextEncoder();
86
- const stream = new ReadableStream<Uint8Array>({
87
- start(controller) {
88
- const send: SseEmit = (event, data) => {
89
- try {
90
- controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
91
- } catch {
92
- /* controller already closed (client disconnected) — nothing to do */
93
- }
94
- };
95
- void (async () => {
96
- try {
97
- const { payload } = await work(send);
98
- send("result", payload);
99
- } catch (error) {
100
- send("result", onError(error).payload);
101
- } finally {
102
- try {
103
- controller.close();
104
- } catch {
105
- /* already closed */
106
- }
107
- }
108
- })();
109
- },
110
- });
111
- return new Response(stream, {
112
- status: 200,
113
- headers: {
114
- "content-type": "text/event-stream; charset=utf-8",
115
- "cache-control": "no-cache, no-transform",
116
- "x-accel-buffering": "no",
117
- },
118
- });
119
- }