Harden live release-manifest readiness checks
This commit is contained in:
parent
fbb623ea2e
commit
58cb19955b
4 changed files with 339 additions and 1 deletions
|
|
@ -37,7 +37,8 @@ Before using these templates:
|
|||
from the local repo to stage the current committed `website/` tree into a VPS
|
||||
temp checkout, boot it on the host-safe staging port, verify the real
|
||||
`/health`, `/api/auth/health`, `/api/releases/manifest`, and root shell
|
||||
surfaces, and clean the temp lane back up on success; add
|
||||
surfaces, fail if the live anonymous release-manifest drifted from the
|
||||
rendered launch authority, and clean the temp lane back up on success; add
|
||||
`--archive-source worktree` when the proof should use the in-progress local
|
||||
worktree rather than committed `HEAD`
|
||||
- after staging proof or for direct root-owned rollout, you can now also run
|
||||
|
|
|
|||
|
|
@ -69,6 +69,27 @@ function isHttpsUrl(url) {
|
|||
return url?.protocol === 'https:'
|
||||
}
|
||||
|
||||
const RELEASE_MANIFEST_PLATFORM_EXPECTATIONS = [
|
||||
{
|
||||
platformKey: 'windows',
|
||||
envPrefix: 'WINDOWS',
|
||||
legacyFrontendUrlKey: 'VITE_WINDOWS_DOWNLOAD_URL',
|
||||
label: 'Windows',
|
||||
},
|
||||
{
|
||||
platformKey: 'macos',
|
||||
envPrefix: 'MACOS',
|
||||
legacyFrontendUrlKey: 'VITE_MAC_DOWNLOAD_URL',
|
||||
label: 'macOS',
|
||||
},
|
||||
{
|
||||
platformKey: 'linux',
|
||||
envPrefix: 'LINUX',
|
||||
legacyFrontendUrlKey: 'VITE_LINUX_DOWNLOAD_URL',
|
||||
label: 'Linux',
|
||||
},
|
||||
]
|
||||
|
||||
function createBucket() {
|
||||
return {
|
||||
failures: [],
|
||||
|
|
@ -202,6 +223,189 @@ function resolveEnvCandidate(frontendEnv, serverEnv, frontendKey, serverKey) {
|
|||
}
|
||||
}
|
||||
|
||||
function readEnvFirst(frontendEnv, serverEnv, frontendKey, serverKey = frontendKey) {
|
||||
return normalizeTrimmed(frontendEnv[frontendKey] || serverEnv[serverKey])
|
||||
}
|
||||
|
||||
function readServerOnly(serverEnv, key) {
|
||||
return normalizeTrimmed(serverEnv[key])
|
||||
}
|
||||
|
||||
function readOptionalInteger(serverEnv, key) {
|
||||
const rawValue = readServerOnly(serverEnv, key)
|
||||
if (!rawValue) {
|
||||
return null
|
||||
}
|
||||
|
||||
const value = Number(rawValue)
|
||||
if (!Number.isFinite(value) || value < 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return Math.trunc(value)
|
||||
}
|
||||
|
||||
function formatRuntimeComparableValue(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return '(blank)'
|
||||
}
|
||||
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? 'true' : 'false'
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return Number.isFinite(value) ? String(value) : '(blank)'
|
||||
}
|
||||
|
||||
const trimmed = normalizeTrimmed(value)
|
||||
return trimmed || '(blank)'
|
||||
}
|
||||
|
||||
function compareRuntimeComparableValues(expected, actual) {
|
||||
return formatRuntimeComparableValue(expected) === formatRuntimeComparableValue(actual)
|
||||
}
|
||||
|
||||
function pushLiveManifestDriftFailure(failures, label, expected, actual) {
|
||||
failures.push(
|
||||
`${label} drifted from configured launch authority (expected ${formatRuntimeComparableValue(expected)}, got ${formatRuntimeComparableValue(actual)}).`,
|
||||
)
|
||||
}
|
||||
|
||||
function appendLiveReleaseManifestDriftChecks({
|
||||
failures,
|
||||
frontendEnv,
|
||||
serverEnv,
|
||||
liveReleaseManifest,
|
||||
}) {
|
||||
const manifest = liveReleaseManifest?.manifest
|
||||
if (!manifest) {
|
||||
return
|
||||
}
|
||||
|
||||
const topLevelExpectations = [
|
||||
{
|
||||
label: 'Live release manifest support email',
|
||||
expected: readEnvFirst(frontendEnv, serverEnv, 'VITE_SUPPORT_EMAIL', 'SUPPORT_EMAIL') || 'hello@hypertwist.app',
|
||||
actual: manifest.support_email,
|
||||
},
|
||||
{
|
||||
label: 'Live release manifest public docs URL',
|
||||
expected: readEnvFirst(frontendEnv, serverEnv, 'VITE_PUBLIC_DOCS_URL', 'PUBLIC_DOCS_URL'),
|
||||
actual: manifest.public_docs_url,
|
||||
},
|
||||
{
|
||||
label: 'Live release manifest release-notes URL',
|
||||
expected: readEnvFirst(frontendEnv, serverEnv, 'VITE_RELEASE_NOTES_URL', 'RELEASE_NOTES_URL'),
|
||||
actual: manifest.release_notes_url,
|
||||
},
|
||||
{
|
||||
label: 'Live release manifest corresponding-source URL',
|
||||
expected: readEnvFirst(frontendEnv, serverEnv, 'VITE_MPL_SOURCE_URL', 'MPL_SOURCE_URL'),
|
||||
actual: manifest.corresponding_source_url,
|
||||
},
|
||||
{
|
||||
label: 'Live release manifest open-source repo URL',
|
||||
expected: readEnvFirst(frontendEnv, serverEnv, 'VITE_OPEN_SOURCE_REPO_URL', 'OPEN_SOURCE_REPO_URL'),
|
||||
actual: manifest.open_source_repo_url,
|
||||
},
|
||||
{
|
||||
label: 'Live release manifest operator checkout URL',
|
||||
expected: readEnvFirst(frontendEnv, serverEnv, 'VITE_PADDLE_CHECKOUT_URL_OPERATOR'),
|
||||
actual: manifest.commerce?.operator_checkout_url,
|
||||
},
|
||||
{
|
||||
label: 'Live release manifest studio checkout URL',
|
||||
expected: readEnvFirst(frontendEnv, serverEnv, 'VITE_PADDLE_CHECKOUT_URL_STUDIO'),
|
||||
actual: manifest.commerce?.studio_checkout_url,
|
||||
},
|
||||
{
|
||||
label: 'Live release manifest operator price string',
|
||||
expected: readEnvFirst(frontendEnv, serverEnv, 'VITE_PLAN_PRICE_OPERATOR') || 'Launch pricing via Paddle',
|
||||
actual: manifest.commerce?.plan_price_operator,
|
||||
},
|
||||
{
|
||||
label: 'Live release manifest studio price string',
|
||||
expected: readEnvFirst(frontendEnv, serverEnv, 'VITE_PLAN_PRICE_STUDIO') || 'Contact for launch readiness',
|
||||
actual: manifest.commerce?.plan_price_studio,
|
||||
},
|
||||
]
|
||||
|
||||
for (const expectation of topLevelExpectations) {
|
||||
if (!compareRuntimeComparableValues(expectation.expected, expectation.actual)) {
|
||||
pushLiveManifestDriftFailure(failures, expectation.label, expectation.expected, expectation.actual)
|
||||
}
|
||||
}
|
||||
|
||||
const platforms = Array.isArray(manifest.platforms) ? manifest.platforms : []
|
||||
const globalVersion = readServerOnly(serverEnv, 'RELEASE_MANIFEST_VERSION')
|
||||
const globalChannel = readServerOnly(serverEnv, 'RELEASE_MANIFEST_CHANNEL') || 'preview'
|
||||
|
||||
for (const definition of RELEASE_MANIFEST_PLATFORM_EXPECTATIONS) {
|
||||
const platform = platforms.find((entry) => entry?.platform_key === definition.platformKey)
|
||||
if (!platform) {
|
||||
failures.push(`Live release manifest is missing the ${definition.label} platform entry.`)
|
||||
continue
|
||||
}
|
||||
|
||||
const configuredCandidate = resolveEnvCandidate(
|
||||
frontendEnv,
|
||||
serverEnv,
|
||||
definition.legacyFrontendUrlKey,
|
||||
`${definition.envPrefix}_DOWNLOAD_URL`,
|
||||
)
|
||||
const expectedConfigured = Boolean(normalizeTrimmed(configuredCandidate.value))
|
||||
const platformExpectations = [
|
||||
{
|
||||
label: `Live release manifest ${definition.label} configured posture`,
|
||||
expected: expectedConfigured,
|
||||
actual: platform.configured === true,
|
||||
},
|
||||
{
|
||||
label: `Live release manifest ${definition.label} release channel`,
|
||||
expected: readServerOnly(serverEnv, `${definition.envPrefix}_RELEASE_CHANNEL`) || globalChannel,
|
||||
actual: platform.channel,
|
||||
},
|
||||
{
|
||||
label: `Live release manifest ${definition.label} release version`,
|
||||
expected: readServerOnly(serverEnv, `${definition.envPrefix}_RELEASE_VERSION`) || globalVersion,
|
||||
actual: platform.version,
|
||||
},
|
||||
{
|
||||
label: `Live release manifest ${definition.label} release build ID`,
|
||||
expected: readServerOnly(serverEnv, `${definition.envPrefix}_RELEASE_BUILD_ID`),
|
||||
actual: platform.build_id,
|
||||
},
|
||||
{
|
||||
label: `Live release manifest ${definition.label} published-at timestamp`,
|
||||
expected: readServerOnly(serverEnv, `${definition.envPrefix}_RELEASE_PUBLISHED_AT`),
|
||||
actual: platform.published_at,
|
||||
},
|
||||
{
|
||||
label: `Live release manifest ${definition.label} release file name`,
|
||||
expected: readServerOnly(serverEnv, `${definition.envPrefix}_RELEASE_FILE_NAME`),
|
||||
actual: platform.file_name,
|
||||
},
|
||||
{
|
||||
label: `Live release manifest ${definition.label} release file size`,
|
||||
expected: readOptionalInteger(serverEnv, `${definition.envPrefix}_RELEASE_FILE_SIZE_BYTES`),
|
||||
actual: platform.file_size_bytes,
|
||||
},
|
||||
{
|
||||
label: `Live release manifest ${definition.label} release checksum`,
|
||||
expected: readServerOnly(serverEnv, `${definition.envPrefix}_RELEASE_SHA256`),
|
||||
actual: platform.checksum_sha256,
|
||||
},
|
||||
]
|
||||
|
||||
for (const expectation of platformExpectations) {
|
||||
if (!compareRuntimeComparableValues(expectation.expected, expectation.actual)) {
|
||||
pushLiveManifestDriftFailure(failures, expectation.label, expectation.expected, expectation.actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function requireAbsoluteUrlCandidate(bucket, label, value) {
|
||||
if (!requireNonEmpty(bucket, label, value)) {
|
||||
return null
|
||||
|
|
@ -571,6 +775,13 @@ export function buildRuntimeReadinessReport({
|
|||
if (liveReleaseManifest.manifest?.viewer?.authenticated !== false || liveReleaseManifest.manifest?.viewer?.canDownload !== false) {
|
||||
failures.push('Live public release manifest viewer posture is not anonymous/non-downloadable.')
|
||||
}
|
||||
|
||||
appendLiveReleaseManifestDriftChecks({
|
||||
failures,
|
||||
frontendEnv,
|
||||
serverEnv,
|
||||
liveReleaseManifest,
|
||||
})
|
||||
} else if (!liveReleaseManifestAttempted) {
|
||||
warnings.push('Live release manifest was not checked; public download-lane runtime posture was evaluated from env posture only.')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -416,6 +416,126 @@ describe('buildRuntimeReadinessReport', () => {
|
|||
expect(report.failures).toContain('Live website root is missing the first-party HyperTwist shell marker.')
|
||||
expect(report.failures).toContain('Live website root is missing the expected #root app mount.')
|
||||
})
|
||||
|
||||
it('fails when the live release manifest drifts from the configured launch authority', () => {
|
||||
const report = buildRuntimeReadinessReport({
|
||||
frontendEnv: {
|
||||
VITE_SUPPORT_EMAIL: 'ops@hypertwist.app',
|
||||
VITE_PUBLIC_DOCS_URL: 'https://docs.hypertwist.app',
|
||||
VITE_RELEASE_NOTES_URL: 'https://notes.hypertwist.app',
|
||||
VITE_SUPERTOKENS_API_DOMAIN: 'https://hypertwist.app',
|
||||
VITE_SUPERTOKENS_WEBSITE_DOMAIN: 'https://hypertwist.app',
|
||||
VITE_AUTH_API_BASE_URL: 'https://hypertwist.app',
|
||||
VITE_PADDLE_CHECKOUT_URL_OPERATOR: 'https://buy.paddle.com/operator-live',
|
||||
VITE_PADDLE_CHECKOUT_URL_STUDIO: 'https://buy.paddle.com/studio-live',
|
||||
VITE_PLAN_PRICE_OPERATOR: '$19 / month',
|
||||
VITE_PLAN_PRICE_STUDIO: '$99 / month',
|
||||
VITE_MPL_SOURCE_URL: 'https://hypertwist.app/open-source/source.zip',
|
||||
VITE_OPEN_SOURCE_REPO_URL: 'https://git.scriptoriumai.io/scriptoriumadmin/hypertwist',
|
||||
VITE_WINDOWS_DOWNLOAD_URL: 'https://downloads.hypertwist.app/windows.exe',
|
||||
},
|
||||
serverEnv: {
|
||||
API_DOMAIN: 'https://hypertwist.app',
|
||||
WEBSITE_DOMAIN: 'https://hypertwist.app',
|
||||
SUPERTOKENS_CORE_URI: 'https://auth-core.internal',
|
||||
COOKIE_SECURE: 'true',
|
||||
PADDLE_WEBHOOK_SECRET: 'secret',
|
||||
PADDLE_PRICE_PLAN_MAP: '{"pri_operator":"operator"}',
|
||||
RELEASE_MANIFEST_VERSION: '1.0.0',
|
||||
RELEASE_MANIFEST_CHANNEL: 'candidate',
|
||||
WINDOWS_RELEASE_BUILD_ID: 'win64-1000',
|
||||
WINDOWS_RELEASE_SHA256: 'abc123',
|
||||
WINDOWS_RELEASE_FILE_SIZE_BYTES: '1048576',
|
||||
},
|
||||
liveHealth: {
|
||||
supertokens: { ready: true },
|
||||
fallback: { active: false },
|
||||
runtime: {
|
||||
public_origin_ready: true,
|
||||
mode: 'public',
|
||||
errors: [],
|
||||
warnings: [],
|
||||
},
|
||||
billing: {
|
||||
webhookSecretConfigured: true,
|
||||
productPlanMapConfigured: false,
|
||||
pricePlanMapConfigured: true,
|
||||
},
|
||||
},
|
||||
liveReleaseManifest: {
|
||||
ok: true,
|
||||
manifest: {
|
||||
support_email: 'hello@hypertwist.app',
|
||||
public_docs_url: 'https://old-docs.hypertwist.app',
|
||||
release_notes_url: 'https://notes.hypertwist.app',
|
||||
corresponding_source_url: 'https://hypertwist.app/open-source/source.zip',
|
||||
open_source_repo_url: 'https://git.scriptoriumai.io/scriptoriumadmin/hypertwist',
|
||||
commerce: {
|
||||
operator_checkout_url: 'https://buy.paddle.com/operator-old',
|
||||
studio_checkout_url: 'https://buy.paddle.com/studio-live',
|
||||
plan_price_operator: '$29 / month',
|
||||
plan_price_studio: '$99 / month',
|
||||
},
|
||||
viewer: {
|
||||
authenticated: false,
|
||||
canDownload: false,
|
||||
},
|
||||
platforms: [
|
||||
{
|
||||
platform_key: 'windows',
|
||||
configured: false,
|
||||
channel: 'preview',
|
||||
version: '0.9.0',
|
||||
build_id: 'old-build',
|
||||
published_at: null,
|
||||
file_name: null,
|
||||
file_size_bytes: 512,
|
||||
checksum_sha256: 'stale',
|
||||
download_url: null,
|
||||
},
|
||||
{
|
||||
platform_key: 'macos',
|
||||
configured: false,
|
||||
channel: 'preview',
|
||||
version: null,
|
||||
build_id: null,
|
||||
published_at: null,
|
||||
file_name: null,
|
||||
file_size_bytes: null,
|
||||
checksum_sha256: null,
|
||||
download_url: null,
|
||||
},
|
||||
{
|
||||
platform_key: 'linux',
|
||||
configured: false,
|
||||
channel: 'preview',
|
||||
version: null,
|
||||
build_id: null,
|
||||
published_at: null,
|
||||
file_name: null,
|
||||
file_size_bytes: null,
|
||||
checksum_sha256: null,
|
||||
download_url: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
liveReleaseManifestAttempted: true,
|
||||
liveHealthAttempted: true,
|
||||
})
|
||||
|
||||
expect(report.ok).toBe(false)
|
||||
expect(report.failures).toContain('Live release manifest support email drifted from configured launch authority (expected ops@hypertwist.app, got hello@hypertwist.app).')
|
||||
expect(report.failures).toContain('Live release manifest public docs URL drifted from configured launch authority (expected https://docs.hypertwist.app, got https://old-docs.hypertwist.app).')
|
||||
expect(report.failures).toContain('Live release manifest operator checkout URL drifted from configured launch authority (expected https://buy.paddle.com/operator-live, got https://buy.paddle.com/operator-old).')
|
||||
expect(report.failures).toContain('Live release manifest operator price string drifted from configured launch authority (expected $19 / month, got $29 / month).')
|
||||
expect(report.failures).toContain('Live release manifest Windows configured posture drifted from configured launch authority (expected true, got false).')
|
||||
expect(report.failures).toContain('Live release manifest Windows release channel drifted from configured launch authority (expected candidate, got preview).')
|
||||
expect(report.failures).toContain('Live release manifest Windows release version drifted from configured launch authority (expected 1.0.0, got 0.9.0).')
|
||||
expect(report.failures).toContain('Live release manifest Windows release build ID drifted from configured launch authority (expected win64-1000, got old-build).')
|
||||
expect(report.failures).toContain('Live release manifest Windows release file size drifted from configured launch authority (expected 1048576, got 512).')
|
||||
expect(report.failures).toContain('Live release manifest Windows release checksum drifted from configured launch authority (expected abc123, got stale).')
|
||||
})
|
||||
})
|
||||
|
||||
describe('deriveHealthBaseUrl', () => {
|
||||
|
|
|
|||
|
|
@ -178,6 +178,12 @@ That means the verifier can now fail explicitly when `hypertwist.app` is still
|
|||
serving the earlier placeholder rollout page instead of the real same-origin
|
||||
website/auth-server deployment.
|
||||
|
||||
The same verifier now also compares the live anonymous release-manifest payload
|
||||
against the currently configured launch authority for support/docs/source,
|
||||
checkout/price, and per-platform release metadata, so a stale host env or
|
||||
partially rolled deploy fails as configuration drift instead of passing on
|
||||
structural safety alone.
|
||||
|
||||
Live same-origin status on `2026-06-22`:
|
||||
|
||||
- the root-owned cutover now has `https://hypertwist.app` serving the
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue