@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.
- package/package.json +1 -1
- package/scripts/lib/tenant-dirs.mjs +80 -0
- package/scripts/tenant/lint-embeds.mjs +180 -0
- package/scripts/tenant/materialize-tenant-content.mjs +533 -0
- package/scripts/tenant/validate.mjs +837 -0
- package/apps/storefront/scripts/build-v84-clean-clone-standalone.mjs +0 -68
- package/apps/storefront/scripts/deny-v84-clean-clone-standalone-capabilities.mjs +0 -111
- package/apps/storefront/scripts/deny-v84-dogfood-capabilities.mjs +0 -2
- package/apps/storefront/scripts/deny-v84-lifecycle-capabilities.mjs +0 -247
- package/apps/storefront/scripts/dogfood-v84-nonportable-routes.ts +0 -73
- package/apps/storefront/scripts/fixed-v84-native-install-producer.ts +0 -79
- package/apps/storefront/scripts/generated/v84-clean-clone-replay.bundle.mjs +0 -10982
- package/apps/storefront/scripts/import-v84-through-ws2.ts +0 -38
- package/apps/storefront/scripts/initialize-v84-lifecycle-tooling-dependency.ts +0 -2
- package/apps/storefront/scripts/initialize-v84-lifecycle-tooling.ts +0 -4
- package/apps/storefront/scripts/install-v84-native-through-ws1.ts +0 -185
- package/apps/storefront/scripts/prove-clone-isolation.ts +0 -506
- package/apps/storefront/scripts/replay-v84-clean-clone-pass.ts +0 -148
- package/apps/storefront/scripts/replay-v84-clean-clone-standalone.ts +0 -35
- package/apps/storefront/scripts/replay-v84-clean-clone.ts +0 -214
- package/apps/storefront/scripts/run-fixed-v84-native-install-producer.mjs +0 -18
- package/apps/storefront/scripts/run-v84-clone-isolation-proof.mjs +0 -18
- package/apps/storefront/scripts/run-v84-clone-lifecycle-proof.mjs +0 -18
- package/apps/storefront/scripts/run-v84-import-through-ws2-proof.mjs +0 -17
- package/apps/storefront/scripts/run-v84-nonportable-route-dogfood.mjs +0 -15
- package/apps/storefront/scripts/sanitize-v84-proof-environment.mjs +0 -21
- package/apps/storefront/scripts/v84CleanCloneGraphPin.ts +0 -42
- package/apps/storefront/scripts/v84CleanCloneSourceGraph.ts +0 -145
- package/apps/storefront/scripts/validate-v84-clone-lifecycle.ts +0 -18
- package/apps/storefront/src/components/admin/AdminAIWorkflowsTab.astro +0 -531
- package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +0 -878
- package/apps/storefront/src/components/admin/AdminBlogTab.astro +0 -1435
- package/apps/storefront/src/components/admin/AdminCheckoutSettingsPanel.astro +0 -226
- package/apps/storefront/src/components/admin/AdminCustomersTab.astro +0 -593
- package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +0 -465
- package/apps/storefront/src/components/admin/AdminIntegrationsTab.astro +0 -105
- package/apps/storefront/src/components/admin/AdminProductsTab.astro +0 -901
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +0 -2983
- package/apps/storefront/src/components/admin/AdminReportingTab.astro +0 -435
- package/apps/storefront/src/components/admin/AdminSettingsTab.astro +0 -782
- package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +0 -611
- package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +0 -1223
- package/apps/storefront/src/lib/dev/cockpitStore.ts +0 -57
- package/apps/storefront/src/lib/publish/adminPublishTab.ts +0 -4625
- package/apps/storefront/src/lib/publish/apex-readiness.ts +0 -360
- package/apps/storefront/src/lib/publish/buildStream.ts +0 -269
- package/apps/storefront/src/lib/publish/collapsePersistence.ts +0 -72
- package/apps/storefront/src/lib/publish/dispatchHealth.ts +0 -267
- package/apps/storefront/src/lib/publish/domainState.ts +0 -360
- package/apps/storefront/src/lib/publish/lifecycleState.ts +0 -369
- package/apps/storefront/src/lib/publish/liveReleaseSummary.ts +0 -29
- package/apps/storefront/src/lib/publish/mergeDoctorOnFailure.ts +0 -78
- package/apps/storefront/src/lib/publish/mergeHealthBanner.ts +0 -62
- package/apps/storefront/src/lib/publish/mergeRowFindings.ts +0 -124
- package/apps/storefront/src/lib/publish/mergeSupportEscalation.ts +0 -75
- package/apps/storefront/src/lib/publish/panelPolling.ts +0 -78
- package/apps/storefront/src/lib/publish/publish-guard.ts +0 -88
- package/apps/storefront/src/lib/publish/publishFormat.ts +0 -165
- package/apps/storefront/src/lib/publish/publishHero.ts +0 -177
- package/apps/storefront/src/lib/publish/reviewRelease/changeState.ts +0 -66
- package/apps/storefront/src/lib/publish/reviewRelease/copy.ts +0 -141
- package/apps/storefront/src/lib/publish/reviewRelease/index.ts +0 -41
- package/apps/storefront/src/lib/publish/reviewRelease/permissions.ts +0 -47
- package/apps/storefront/src/lib/publish/reviewRelease/releaseState.ts +0 -42
- package/apps/storefront/src/lib/publish/reviewRelease/types.ts +0 -59
- package/apps/storefront/src/lib/publish/reviewRelease/viewModel.ts +0 -440
- package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/actions.ts +0 -161
- package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/controller.ts +0 -325
- package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/render.ts +0 -241
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +0 -439
- package/apps/storefront/src/lib/publish/sseRoute.ts +0 -119
- package/apps/storefront/src/lib/publish/stageBundleUpload.ts +0 -215
- package/apps/storefront/src/lib/publish/streamLogConsole.ts +0 -142
- package/apps/storefront/src/lib/publish/versionNumber.ts +0 -52
- package/apps/storefront/src/pages/admin/ops-timeline.astro +0 -420
- package/apps/storefront/src/pages/admin/review-release.astro +0 -265
- package/apps/storefront/src/pages/admin/select.astro +0 -96
- package/apps/storefront/src/pages/admin.astro +0 -570
- package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +0 -86
- package/apps/storefront/src/pages/api/apps/admin/gtm-migration-install.ts +0 -174
- package/apps/storefront/src/pages/api/apps/admin/gtm-migration.ts +0 -96
- package/apps/storefront/src/pages/api/apps/admin/health.ts +0 -44
- package/apps/storefront/src/pages/api/apps/admin/install.ts +0 -125
- package/apps/storefront/src/pages/api/apps/admin/integration-lifecycle.ts +0 -118
- package/apps/storefront/src/pages/api/apps/admin/integrations.ts +0 -89
- package/apps/storefront/src/pages/api/apps/admin/list.ts +0 -26
- package/apps/storefront/src/pages/api/apps/admin/platform-adapters.ts +0 -85
- package/apps/storefront/src/pages/api/apps/admin/resume.ts +0 -79
- package/apps/storefront/src/pages/api/apps/admin/suspend.ts +0 -80
- package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +0 -98
- package/apps/storefront/src/pages/api/apps/admin/update.ts +0 -126
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +0 -70
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +0 -51
- package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +0 -255
- package/apps/storefront/src/pages/api/apps/oauth/token.ts +0 -87
- package/apps/storefront/src/pages/api/apps/v1/attribution.ts +0 -162
- package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +0 -39
- package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +0 -49
- package/apps/storefront/src/pages/api/apps/v1/health.ts +0 -32
- package/apps/storefront/src/pages/api/apps/v1/inventory.ts +0 -59
- package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +0 -48
- package/apps/storefront/src/pages/api/apps/v1/orders.ts +0 -73
- package/apps/storefront/src/pages/api/apps/v1/reports.ts +0 -21
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +0 -74
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +0 -47
- package/apps/storefront/src/pages/api/cache-purge.ts +0 -55
- package/apps/storefront/src/pages/api/compliance/tamper.ts +0 -88
- package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +0 -154
- package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +0 -120
- package/apps/storefront/src/pages/api/integrations/v1/[operation].ts +0 -108
- package/apps/storefront/src/pages/api/publish/stage-bundle.ts +0 -70
- package/apps/storefront/src/pages/api/test/dev-session.ts +0 -133
- package/apps/storefront/src/pages/cockpit.astro +0 -1548
- package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +0 -119
- package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +0 -105
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +0 -177
- 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
|
-
}
|