@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,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
- }