@growth-labs/conformance
v0.16.0
Published
Reusable fleet publication conformance harness for Growth Labs sites: headers, cache, metadata, structured data, images, publication surfaces, and evidence bundles.
Readme
@growth-labs/conformance
Reusable publication conformance harness for Growth Labs sites. It validates per-site fixtures and emits machine-parseable evidence bundles for render, cache, security headers, metadata, structured data, images, publication surfaces, Discover eligibility, and build evidence.
This package is consumed by sites and CI jobs. It is not a central fleet runtime and it does not fetch, mutate, deploy, or store fleet state.
Public or crawler fixtures can explicitly approve scoped inline styles with
expected.publicInlineStyles: { attributes: true, elements: true }. Both booleans
are required; each true requires the corresponding style-src-attr or
style-src-elem 'unsafe-inline' grant in the actual enforced CSP. Omission or
false retains the existing strict/attribute-inventory validation. This contract
does not allow private audiences, simultaneous receipt-free attributes and an
inventory receipt, or unsafe-inline in scripts, default-src, or unscoped style-src.
Cache Safety
import {
applyPrivateNoStore,
evaluateCacheSafety,
scanRequestStateLeaks,
} from '@growth-labs/conformance/cache-safety'The cache-safety contract keeps the default gateway uncached, requires an empty
public HTML allowlist, verifies a non-public and side-effect-free inner renderer
with an explicit forwarded-header allowlist, and proves HIT still executes the
outer gateway side effects. applyPrivateNoStore() writes the complete
Cache-Control/CDN-Cache-Control/Cloudflare-CDN-Cache-Control triplet and removes
Cookie from Vary.
Omitted cache evidence fails with cache.evidence.missing. A public response
with Set-Cookie fails unless it carries the complete triplet. Vary: Cookie
always fails—even alongside the private triplet—because Cookie variance is not
part of the supported contract; remove it after marking the response private.
The body scanner accepts controlled request-state sentinels and detects their
literal, URI, JSON, and base64 forms. Its receipt exposes only state categories
and encodings—never raw sentinel values. cacheSafetyLiveResultSchema validates
the bounded result.
Route conformance for a built Worker
runWorkerRoutes() (subpath @growth-labs/conformance/worker-routes, Node only, needs the
optional peer miniflare) boots the built Worker from dist/server in Miniflare with a small
committed D1 fixture and requests the routes a publication must answer. It asserts status,
<title>, canonical, JSON-LD type and og:image (and content type and a body marker for the
feeds, sitemap and robots.txt), and returns the result as conformance findings.
import { defaultRoutes, formatRouteReport } from '@growth-labs/conformance'
import { runWorkerRoutes } from '@growth-labs/conformance/worker-routes'
const report = await runWorkerRoutes({
site: 'megaprojects',
origin: 'https://megaprojects.pub',
serverDir: new URL('../dist/server/', import.meta.url).pathname,
bindings: { SITE_URL: 'https://megaprojects.pub' },
d1: {
SITE_DB: {
migrationsDir: 'migrations', // the site's own migrations, applied in file-name order
sql: readFileSync('tests/fixtures/routes.sql', 'utf8'), // optional; one statement per line
seed: async (db) => {}, // optional; seed through the app's code, e.g. the CMS engine
},
},
stubServices: ['ASSETS', 'AUTH'], // answer 503 unless `serviceBindings` supplies one
routes: defaultRoutes({ article: '/articles/newest', video: '/videos/first' }),
})
if (report.status === 'fail') throw new Error(formatRouteReport(report))Finding codes: route.status.unexpected (a 500 on / is this one, with the start of the body as
evidence), route.content_type.unexpected, route.body.expected_missing, route.title.missing,
route.canonical.missing, route.canonical.mismatch, route.structured_data.type_missing,
route.og_image.missing, route.fetch.failed. A route spec can override any expectation
(status may be a list, jsonLdTypes entries may be alternatives). The pure half
(evaluateRoute, buildRouteReport) runs on responses captured any other way.
Install
pnpm add @growth-labs/conformanceBasic Usage
import { runFleetConformance, runSiteConformance } from '@growth-labs/conformance'
const fixture = {
id: 'fronts:article:public',
site: 'fronts',
environment: 'production',
url: 'https://fronts.co/example',
renderMode: 'ssr',
audience: 'public',
response: {
status: 200,
headers: {
'content-security-policy': "default-src 'self'; script-src 'self' 'nonce-runtime'",
'strict-transport-security': 'max-age=31536000',
'cross-origin-opener-policy': 'same-origin',
'cross-origin-resource-policy': 'same-origin',
'x-frame-options': 'DENY',
'x-content-type-options': 'nosniff',
'referrer-policy': 'strict-origin-when-cross-origin',
'permissions-policy': 'camera=(), microphone=(), geolocation=()',
'cache-control': 'public, max-age=0, s-maxage=300',
},
html: '<!doctype html>...',
},
expected: {
canonicalUrl: 'https://fronts.co/example',
schemaTypes: ['NewsArticle'],
discoverEligible: true,
framePolicy: {
ancestors: ["'none'"],
legacyHeader: 'DENY',
},
},
publicationSurfaces: [
{ name: 'sitemap', status: 'present', httpStatus: 200, bodyBytes: 1024 },
{ name: 'robots', status: 'present', httpStatus: 200, bodyBytes: 256 },
{ name: 'feed', status: 'na', capabilityBoundary: 'No editorial feed', evidence: 'site type' },
],
cacheVariants: [
{ audience: 'public', cacheKey: 'fronts:public:/example' },
{ audience: 'crawler', cacheKey: 'fronts:crawler:/example' },
],
cacheSafety: {
defaultGatewayCacheEnabled: false,
publicHtmlAllowlist: [],
inner: {
publiclyRoutable: false,
forwardedHeaders: ['accept-language'],
allowedForwardedHeaders: ['accept-language'],
sideEffects: false,
},
gateway: { hitRunsSideEffects: true },
response: {
personalized: false,
headers: { 'cache-control': 'public, s-maxage=300' },
body: '<!doctype html>...',
sentinels: [],
},
},
securityReceipts: {
functionalProbes: [
{ name: 'oauth', status: 'pass', evidence: 'sanitized consumer probe receipt' },
{ name: 'media', status: 'pass', evidence: 'sanitized consumer probe receipt' },
{ name: 'images', status: 'pass', evidence: 'sanitized consumer probe receipt' },
{ name: 'analytics', status: 'pass', evidence: 'sanitized consumer probe receipt' },
{ name: 'embeds', status: 'pass', evidence: 'sanitized consumer probe receipt' },
{ name: 'error-pages', status: 'pass', evidence: 'sanitized consumer probe receipt' },
],
cspReports: [
{
name: 'csp-report-canary',
status: 'pass',
endpoint: 'consumer-owned-report-endpoint',
evidence: 'sanitized CSP report or blocked-canary receipt',
},
],
inlineStyleAttributes: {
commitSha: 'bd39d718f6a5b0df9a249e11638a8b430050941e',
occurrenceCount: 4,
evidence: {
tool: 'platform-foundations:inline-style-inventory@1',
outputHash: 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
},
},
},
buildEvidence: {
commitSha: 'bd39d718f6a5b0df9a249e11638a8b430050941e',
packageVersions: { '@growth-labs/conformance': '0.6.0', '@growth-labs/seo': '0.9.0' },
staticHeaderPolicy: true,
ssrHeaderPolicy: true,
},
}
const siteBundle = runSiteConformance(fixture)
const fleetBundle = runFleetConformance([fixture])Deployment Endpoint Adapter
Consumers own the route, the conformance artifact, and the Cloudflare
[version_metadata] binding. The CI/build probe step runs
runSiteConformance(), serializes the result to a JSON artifact file, and
bundles that file with the Worker. The adapter reads the pre-built artifact at
module load (not per request) and serves it:
import artifact from './conformance-artifact.json' with { type: 'json' }
import { initConformanceEndpoint } from '@growth-labs/conformance/endpoint'
// Artifact produced by the CI probe step before deployment.
// Static import: a missing conformance-artifact.json is a build-time error —
// the Worker never starts if the artifact is absent.
// Artifact is parsed, schema-stripped, bounded, and serialized ONCE here.
const handler = initConformanceEndpoint(artifact, 'fronts')
// src/pages/.well-known/growth-labs-conformance.json.ts
export function GET({ locals, request }) {
return handler.handle({
method: request.method,
versionMetadata: locals.runtime.env.CF_VERSION_METADATA,
})
}
export const HEAD = GETinitConformanceEndpoint parses the artifact, validates it against the v2
schema, checks environment: 'production' and the expected site, requires
buildEvidence.commitSha (lowercase 40-character hex) and
buildEvidence.generatedAt (timezone-qualified ISO-8601 datetime), and
serializes the schema-stripped body bytes once. Per-request handling only
validates the method and compares versionMetadata.tag against the baked commit
SHA — no I/O, no probe execution, no re-serialization. Missing/malformed
artifacts and wrong site/environment produce a handler that always returns 503.
The adapter never reads or serializes environment bindings, request headers,
Cloudflare version IDs, Cloudflare version timestamps, internal exceptions, or
fields outside the parsed v2 schema. It does not perform heuristic secret
detection: documented free-form strings such as finding messages, finding
evidence, remediation text, and receipt evidence remain unchanged and are the
consumer's sanitization responsibility.
Shared Header Policy
Use buildSharedPublicationHeaders() when a site wants the package-owned
baseline for SSR and static response headers. Astro sites on releases that
support security.csp should let Astro own the per-route hash CSP and retain
the remaining shared headers. The package peer contract remains the estate's
optional Astro 6 contract:
import {
buildAstroCspConfig,
buildSharedPublicationHeaders,
} from '@growth-labs/conformance/policy'
const csp = buildAstroCspConfig({
scriptSources: ['https://challenges.cloudflare.com'],
styleSources: ['https://fonts.googleapis.com'],
})
const headers = buildSharedPublicationHeaders({
renderMode: 'ssr',
audience: 'public',
contentSecurityPolicy: false,
markdownTwinUrl: 'https://example.com/article.md',
})Analytics tag sources (@growth-labs/analytics)
Sites that enable the @growth-labs/analytics external tags must add the tags'
exact origins to the SecurityHeadersConfig allowlists (same exact-HTTPS-origin
form as the examples above — no wildcards for script hosts). Both tags are
external <script src> with no inline bootstrap, so hash-based CSP needs only
these origins, not a per-render inline hash:
const csp = buildAstroCspConfig({
scriptSources: [
'https://static.cloudflareinsights.com', // Cloudflare Web Analytics beacon (cookieless)
'https://www.googletagmanager.com', // GA4 gtag.js loader (consent-gated)
],
connectSources: [
'https://cloudflareinsights.com', // CWA RUM upload
'https://www.google-analytics.com', // GA4 collect
'https://*.google-analytics.com',
'https://*.analytics.google.com',
],
imgSources: [
'https://www.google-analytics.com', // GA4 pixel fallback
'https://*.google-analytics.com',
],
})Add a source only for the tags a site actually enables (CWA-only sites omit the GA4 origins, and vice versa).
The fixture default is the non-embeddable policy above. For a deliberately
embeddable route, declare its exact response class and omit a conflicting
X-Frame-Options header:
expected: {
framePolicy: {
ancestors: ['https://trusted.example'],
legacyHeader: 'omit',
},
}Accepted ancestors are 'none', 'self', https:, and exact HTTPS origins;
wildcards, paths, credentials, query strings, and fragments are rejected.
DENY is valid only with 'none', and SAMEORIGIN only with 'self'; broader
embeddable policies must use legacyHeader: 'omit' to avoid contradictory
browser behavior.
Astro 6 security.csp cannot represent attribute-scoped style resources. If a
route still emits HTML style attributes, keep the Astro-native helper
fail-closed and serve a response-header CSP with style-src-attr plus the
validated inlineStyleAttributes receipt in the conformance fixture. The
receipt requires the exact lowercase 40-character build commit SHA, a positive
occurrence count, the versioned scanner identity, and the lowercase SHA-256
content hash of its machine output. The same receipt must appear in the
conformance fixture and its commit must match buildEvidence.commitSha. Do not
use an inline-style allowance as a generic compatibility fix.
The policy emits CSP, HSTS, frame protection, MIME sniffing protection,
COOP, CORP, referrer policy, permissions policy, cache semantics, and optional
markdown twin Link metadata. Private audiences use private, no-store;
public SSR uses short shared-cache semantics; public static surfaces get longer
shared-cache semantics.
HSTS defaults to max-age=31536000 only. Add includeSubDomains and preload
through strictTransportSecurity: { includeSubDomains: true, preload: true }
only after the consumer has separately verified the full host inventory can
enforce HTTPS. COOP defaults to same-origin and CORP defaults to
same-origin; consumers may override them only when verified OAuth popup,
media, or embed constraints require a looser policy and the receipt proves the
result under enforced CSP.
Fixture Commands
The root repo exposes:
pnpm run conformance:test-fixtures
pnpm run conformance:verify-schemaThe fixture command proves the required negative fixtures fail loudly with stable finding codes:
csp-unsafe-inlinemissing-coopmissing-corphsts-below-one-yearcsp-wildcard-default-srccsp-unsafe-evalcsp-inline-executable-without-nonce-or-hashconflicting-duplicate-security-headermissing-functional-probe-receiptmissing-csp-report-receiptfailed-csp-report-receiptheaders-static-only-while-ssr-missingundeclared-nastringified-is-accessible-for-freevideo-object-date-only-upload-datemissing-image-dimensionshero-under-1200pxmissing-max-image-preview-largecrawler-public-cache-key-collisionruntime-canonical-override-not-appliedllms-runtime-static-shadowdefault-entrypoint-cache-enabledpublic-html-allowlist-nonemptyraw-credential-forwarded-to-innercached-body-request-state-leakgateway-hit-side-effects-bypassedpersonalized-public-responsepublic-set-cookie-responsevary-cookie-response
Evidence Shape
runSiteConformance() returns a SiteEvidenceBundle; runFleetConformance()
returns a FleetEvidenceBundle. Both are validated by exported Zod schemas
and carry schemaVersion: "growth-labs.conformance.v2" so downstream jobs can
parse them without relying on console text.
Schema v2 adds securityReceipts to fixtures and site evidence bundles. Existing
v1 fixtures must add explicit functional-probe receipts for oauth, media,
images, analytics, embeds, and error-pages, plus at least one passing CSP
reporting/canary receipt. The package does not perform those live probes itself;
consumer CI supplies sanitized pass/fail/NA receipts.
