@growth-labs/conformance 0.1.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # @growth-labs/conformance Changelog
2
+
3
+ ## 0.1.0 — 2026-07-15
4
+
5
+ ### Added
6
+
7
+ - Initial reusable fleet publication conformance harness.
8
+ - Added Zod schemas for fixtures, site evidence bundles, and fleet evidence
9
+ bundles.
10
+ - Added shared SSR/static header and cache policy helpers.
11
+ - Added deterministic required negative fixtures for CSP, runtime headers,
12
+ declared NA boundaries, JSON-LD booleans, VideoObject upload dates, image
13
+ dimensions, Discover hero width, robots `max-image-preview:large`, cache
14
+ key separation, and runtime canonical overrides.
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # @growth-labs/conformance
2
+
3
+ Reusable publication conformance harness for Growth Labs sites. It validates
4
+ per-site fixtures and emits machine-parseable evidence bundles for render,
5
+ cache, security headers, metadata, structured data, images, publication
6
+ surfaces, Discover eligibility, and build evidence.
7
+
8
+ This package is consumed by sites and CI jobs. It is not a central fleet
9
+ runtime and it does not fetch, mutate, deploy, or store fleet state.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pnpm add @growth-labs/conformance
15
+ ```
16
+
17
+ ## Basic Usage
18
+
19
+ ```ts
20
+ import { runFleetConformance, runSiteConformance } from '@growth-labs/conformance'
21
+
22
+ const fixture = {
23
+ id: 'fronts:article:public',
24
+ site: 'fronts',
25
+ environment: 'production',
26
+ url: 'https://fronts.co/example',
27
+ renderMode: 'ssr',
28
+ audience: 'public',
29
+ response: {
30
+ status: 200,
31
+ headers: {
32
+ 'content-security-policy': "default-src 'self'; script-src 'self' 'nonce-runtime'",
33
+ 'strict-transport-security': 'max-age=31536000; includeSubDomains; preload',
34
+ 'x-frame-options': 'DENY',
35
+ 'x-content-type-options': 'nosniff',
36
+ 'referrer-policy': 'strict-origin-when-cross-origin',
37
+ 'permissions-policy': 'camera=(), microphone=(), geolocation=()',
38
+ 'cache-control': 'public, max-age=0, s-maxage=300',
39
+ },
40
+ html: '<!doctype html>...',
41
+ },
42
+ expected: {
43
+ canonicalUrl: 'https://fronts.co/example',
44
+ schemaTypes: ['NewsArticle'],
45
+ discoverEligible: true,
46
+ },
47
+ publicationSurfaces: [
48
+ { name: 'sitemap', status: 'present', httpStatus: 200, bodyBytes: 1024 },
49
+ { name: 'robots', status: 'present', httpStatus: 200, bodyBytes: 256 },
50
+ { name: 'feed', status: 'na', capabilityBoundary: 'No editorial feed', evidence: 'site type' },
51
+ ],
52
+ cacheVariants: [
53
+ { audience: 'public', cacheKey: 'fronts:public:/example' },
54
+ { audience: 'crawler', cacheKey: 'fronts:crawler:/example' },
55
+ ],
56
+ buildEvidence: {
57
+ commitSha: 'bd39d718f6a5b0df9a249e11638a8b430050941e',
58
+ packageVersions: { '@growth-labs/seo': '0.9.0' },
59
+ staticHeaderPolicy: true,
60
+ ssrHeaderPolicy: true,
61
+ },
62
+ }
63
+
64
+ const siteBundle = runSiteConformance(fixture)
65
+ const fleetBundle = runFleetConformance([fixture])
66
+ ```
67
+
68
+ ## Shared Header Policy
69
+
70
+ Use `buildSharedPublicationHeaders()` when a site wants the package-owned
71
+ baseline for SSR and static response headers:
72
+
73
+ ```ts
74
+ import { buildSharedPublicationHeaders } from '@growth-labs/conformance/policy'
75
+
76
+ const headers = buildSharedPublicationHeaders({
77
+ renderMode: 'ssr',
78
+ audience: 'public',
79
+ nonce: runtimeNonce,
80
+ markdownTwinUrl: 'https://example.com/article.md',
81
+ })
82
+ ```
83
+
84
+ The policy emits CSP, HSTS, frame protection, MIME sniffing protection,
85
+ referrer policy, permissions policy, cache semantics, and optional markdown
86
+ twin `Link` metadata. Private audiences use `private, no-store`; public SSR
87
+ uses short shared-cache semantics; public static surfaces get longer
88
+ shared-cache semantics.
89
+
90
+ ## Fixture Commands
91
+
92
+ The root repo exposes:
93
+
94
+ ```bash
95
+ pnpm run conformance:test-fixtures
96
+ pnpm run conformance:verify-schema
97
+ ```
98
+
99
+ The fixture command proves the required negative fixtures fail loudly with
100
+ stable finding codes:
101
+
102
+ - `csp-unsafe-inline`
103
+ - `headers-static-only-while-ssr-missing`
104
+ - `undeclared-na`
105
+ - `stringified-is-accessible-for-free`
106
+ - `video-object-date-only-upload-date`
107
+ - `missing-image-dimensions`
108
+ - `hero-under-1200px`
109
+ - `missing-max-image-preview-large`
110
+ - `crawler-public-cache-key-collision`
111
+ - `runtime-canonical-override-not-applied`
112
+
113
+ ## Evidence Shape
114
+
115
+ `runSiteConformance()` returns a `SiteEvidenceBundle`; `runFleetConformance()`
116
+ returns a `FleetEvidenceBundle`. Both are validated by exported Zod schemas
117
+ and carry `schemaVersion: "growth-labs.conformance.v1"` so downstream jobs can
118
+ parse them without relying on console text.
package/dist/html.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ export interface JsonLdParseError {
2
+ message: string;
3
+ path: string;
4
+ }
5
+ export interface JsonLdNode {
6
+ [key: string]: unknown;
7
+ }
8
+ export interface ExtractedImage {
9
+ url?: string;
10
+ width?: number;
11
+ height?: number;
12
+ path: string;
13
+ }
14
+ export interface HtmlSignals {
15
+ canonicalUrl?: string;
16
+ robotsContent?: string;
17
+ jsonLdNodes: JsonLdNode[];
18
+ jsonLdParseErrors: JsonLdParseError[];
19
+ jsonLdImages: ExtractedImage[];
20
+ }
21
+ export declare function extractHtmlSignals(html?: string): HtmlSignals;
22
+ export declare function getSchemaTypes(node: JsonLdNode): string[];
23
+ export declare function nodeHasSchemaType(node: JsonLdNode, expectedType: string): boolean;
24
+ //# sourceMappingURL=html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../src/html.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,UAAU;IAC1B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACtB;AAED,MAAM,WAAW,cAAc;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,WAAW;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,WAAW,EAAE,UAAU,EAAE,CAAA;IACzB,iBAAiB,EAAE,gBAAgB,EAAE,CAAA;IACrC,YAAY,EAAE,cAAc,EAAE,CAAA;CAC9B;AAED,wBAAgB,kBAAkB,CAAC,IAAI,SAAK,GAAG,WAAW,CAazD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,EAAE,CAKzD;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAEjF"}
package/dist/html.js ADDED
@@ -0,0 +1,138 @@
1
+ export function extractHtmlSignals(html = '') {
2
+ const jsonLd = extractJsonLdNodes(html);
3
+ const jsonLdImages = jsonLd.nodes.flatMap((node, index) => extractImagesFromJsonLdValue(node.image, `jsonLd[${index}].image`));
4
+ return {
5
+ canonicalUrl: extractCanonicalUrl(html),
6
+ robotsContent: extractMetaContent(html, 'robots'),
7
+ jsonLdNodes: jsonLd.nodes,
8
+ jsonLdParseErrors: jsonLd.errors,
9
+ jsonLdImages,
10
+ };
11
+ }
12
+ export function getSchemaTypes(node) {
13
+ const value = node['@type'];
14
+ if (typeof value === 'string')
15
+ return [value];
16
+ if (Array.isArray(value))
17
+ return value.filter((item) => typeof item === 'string');
18
+ return [];
19
+ }
20
+ export function nodeHasSchemaType(node, expectedType) {
21
+ return getSchemaTypes(node).includes(expectedType);
22
+ }
23
+ function extractCanonicalUrl(html) {
24
+ for (const tag of findOpeningTags(html, 'link')) {
25
+ const attrs = parseAttributes(tag);
26
+ if (attrs.rel?.split(/\s+/).some((value) => value.toLowerCase() === 'canonical')) {
27
+ return attrs.href;
28
+ }
29
+ }
30
+ return undefined;
31
+ }
32
+ function extractMetaContent(html, name) {
33
+ const wanted = name.toLowerCase();
34
+ for (const tag of findOpeningTags(html, 'meta')) {
35
+ const attrs = parseAttributes(tag);
36
+ const metaName = attrs.name?.toLowerCase() ?? attrs.property?.toLowerCase();
37
+ if (metaName === wanted)
38
+ return attrs.content;
39
+ }
40
+ return undefined;
41
+ }
42
+ function extractJsonLdNodes(html) {
43
+ const nodes = [];
44
+ const errors = [];
45
+ const scriptPattern = /<script\b[^>]*>([\s\S]*?)<\/script>/gi;
46
+ let scriptIndex = 0;
47
+ for (const match of html.matchAll(scriptPattern)) {
48
+ const fullTag = match[0] ?? '';
49
+ const openTag = fullTag.slice(0, fullTag.indexOf('>') + 1);
50
+ const attrs = parseAttributes(openTag);
51
+ if ((attrs.type ?? '').toLowerCase() !== 'application/ld+json')
52
+ continue;
53
+ const body = match[1] ?? '';
54
+ try {
55
+ const parsed = JSON.parse(body.trim());
56
+ collectJsonLdNodes(parsed, nodes);
57
+ }
58
+ catch (error) {
59
+ const message = error instanceof Error ? error.message : String(error);
60
+ errors.push({ message, path: `script[application/ld+json][${scriptIndex}]` });
61
+ }
62
+ scriptIndex += 1;
63
+ }
64
+ return { nodes, errors };
65
+ }
66
+ function collectJsonLdNodes(value, out) {
67
+ if (Array.isArray(value)) {
68
+ for (const item of value)
69
+ collectJsonLdNodes(item, out);
70
+ return;
71
+ }
72
+ if (!isRecord(value))
73
+ return;
74
+ out.push(value);
75
+ const graph = value['@graph'];
76
+ if (Array.isArray(graph)) {
77
+ for (const item of graph)
78
+ collectJsonLdNodes(item, out);
79
+ }
80
+ }
81
+ function extractImagesFromJsonLdValue(value, path) {
82
+ if (Array.isArray(value)) {
83
+ return value.flatMap((item, index) => extractImagesFromJsonLdValue(item, `${path}[${index}]`));
84
+ }
85
+ if (typeof value === 'string') {
86
+ return [{ url: value, path }];
87
+ }
88
+ if (!isRecord(value))
89
+ return [];
90
+ const url = firstString(value.url, value.contentUrl, value['@id']);
91
+ const width = toFiniteNumber(value.width);
92
+ const height = toFiniteNumber(value.height);
93
+ return [{ url, width, height, path }];
94
+ }
95
+ function findOpeningTags(html, tagName) {
96
+ const pattern = new RegExp(`<${tagName}\\b[^>]*>`, 'gi');
97
+ return [...html.matchAll(pattern)].map((match) => match[0] ?? '');
98
+ }
99
+ function parseAttributes(tag) {
100
+ const attrs = {};
101
+ const attributePattern = /([A-Za-z_:][-A-Za-z0-9_:.]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/g;
102
+ for (const match of tag.matchAll(attributePattern)) {
103
+ const name = match[1]?.toLowerCase();
104
+ if (!name)
105
+ continue;
106
+ attrs[name] = decodeHtmlEntities(match[2] ?? match[3] ?? match[4] ?? '');
107
+ }
108
+ return attrs;
109
+ }
110
+ function firstString(...values) {
111
+ for (const value of values) {
112
+ if (typeof value === 'string' && value.trim() !== '')
113
+ return value;
114
+ }
115
+ return undefined;
116
+ }
117
+ function toFiniteNumber(value) {
118
+ if (typeof value === 'number' && Number.isFinite(value))
119
+ return value;
120
+ if (typeof value === 'string' && value.trim() !== '') {
121
+ const parsed = Number(value);
122
+ if (Number.isFinite(parsed))
123
+ return parsed;
124
+ }
125
+ return undefined;
126
+ }
127
+ function decodeHtmlEntities(value) {
128
+ return value
129
+ .replaceAll('&amp;', '&')
130
+ .replaceAll('&quot;', '"')
131
+ .replaceAll('&#39;', "'")
132
+ .replaceAll('&lt;', '<')
133
+ .replaceAll('&gt;', '>');
134
+ }
135
+ function isRecord(value) {
136
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
137
+ }
138
+ //# sourceMappingURL=html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.js","sourceRoot":"","sources":["../src/html.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export { buildCacheControl, buildContentSecurityPolicy, buildSharedPublicationHeaders, type CachePolicyOptions, type HeaderPolicyOptions, isPrivateAudience, normalizeHeaders, REQUIRED_SECURITY_HEADERS, } from './policy.js';
2
+ export { type RunConformanceOptions, runFleetConformance, runSiteConformance } from './runner.js';
3
+ export { type Audience, audienceSchema, type BuildEvidence, buildEvidenceSchema, type CacheVariant, type ConformanceCheckReceipt, type ConformanceFinding, type ConformanceFixture, type ConformanceFixtureInput, cacheVariantSchema, checkStatusSchema, conformanceCheckReceiptSchema, conformanceExpectedSchema, conformanceFindingSchema, conformanceFixtureSchema, EVIDENCE_SCHEMA_VERSION, type EvidenceSummary, evidenceSummarySchema, type FleetEvidenceBundle, findingSeveritySchema, fleetEvidenceBundleSchema, type HeaderMap, headerMapSchema, headerValueSchema, type PublicationSurfaceEvidence, parseConformanceFixture, publicationSurfaceSchema, REQUIRED_NEGATIVE_FIXTURE_CODES, type RenderMode, type RequiredNegativeFindingCode, type RequiredNegativeFixtureId, renderModeSchema, type SiteEvidenceBundle, type SurfaceResponse, siteEvidenceBundleSchema, surfaceResponseSchema, validateFleetEvidenceBundle, validateSiteEvidenceBundle, } from './schema.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,iBAAiB,EACjB,0BAA0B,EAC1B,6BAA6B,EAC7B,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,iBAAiB,EACjB,gBAAgB,EAChB,yBAAyB,GACzB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,KAAK,qBAAqB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AACjG,OAAO,EACN,KAAK,QAAQ,EACb,cAAc,EACd,KAAK,aAAa,EAClB,mBAAmB,EACnB,KAAK,YAAY,EACjB,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,uBAAuB,EAC5B,kBAAkB,EAClB,iBAAiB,EACjB,6BAA6B,EAC7B,yBAAyB,EACzB,wBAAwB,EACxB,wBAAwB,EACxB,uBAAuB,EACvB,KAAK,eAAe,EACpB,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,qBAAqB,EACrB,yBAAyB,EACzB,KAAK,SAAS,EACd,eAAe,EACf,iBAAiB,EACjB,KAAK,0BAA0B,EAC/B,uBAAuB,EACvB,wBAAwB,EACxB,+BAA+B,EAC/B,KAAK,UAAU,EACf,KAAK,2BAA2B,EAChC,KAAK,yBAAyB,EAC9B,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,wBAAwB,EACxB,qBAAqB,EACrB,2BAA2B,EAC3B,0BAA0B,GAC1B,MAAM,aAAa,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { buildCacheControl, buildContentSecurityPolicy, buildSharedPublicationHeaders, isPrivateAudience, normalizeHeaders, REQUIRED_SECURITY_HEADERS, } from './policy.js';
2
+ export { runFleetConformance, runSiteConformance } from './runner.js';
3
+ export { audienceSchema, buildEvidenceSchema, cacheVariantSchema, checkStatusSchema, conformanceCheckReceiptSchema, conformanceExpectedSchema, conformanceFindingSchema, conformanceFixtureSchema, EVIDENCE_SCHEMA_VERSION, evidenceSummarySchema, findingSeveritySchema, fleetEvidenceBundleSchema, headerMapSchema, headerValueSchema, parseConformanceFixture, publicationSurfaceSchema, REQUIRED_NEGATIVE_FIXTURE_CODES, renderModeSchema, siteEvidenceBundleSchema, surfaceResponseSchema, validateFleetEvidenceBundle, validateSiteEvidenceBundle, } from './schema.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,iBAAiB,EACjB,0BAA0B,EAC1B,6BAA6B,EAG7B,iBAAiB,EACjB,gBAAgB,EAChB,yBAAyB,GACzB,MAAM,aAAa,CAAA;AACpB,OAAO,EAA8B,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AACjG,OAAO,EAEN,cAAc,EAEd,mBAAmB,EAMnB,kBAAkB,EAClB,iBAAiB,EACjB,6BAA6B,EAC7B,yBAAyB,EACzB,wBAAwB,EACxB,wBAAwB,EACxB,uBAAuB,EAEvB,qBAAqB,EAErB,qBAAqB,EACrB,yBAAyB,EAEzB,eAAe,EACf,iBAAiB,EAEjB,uBAAuB,EACvB,wBAAwB,EACxB,+BAA+B,EAI/B,gBAAgB,EAGhB,wBAAwB,EACxB,qBAAqB,EACrB,2BAA2B,EAC3B,0BAA0B,GAC1B,MAAM,aAAa,CAAA"}
@@ -0,0 +1,18 @@
1
+ import type { Audience, HeaderMap, RenderMode } from './schema.js';
2
+ export declare const REQUIRED_SECURITY_HEADERS: readonly ["content-security-policy", "strict-transport-security", "x-frame-options", "x-content-type-options", "referrer-policy", "permissions-policy"];
3
+ export interface HeaderPolicyOptions {
4
+ renderMode: RenderMode;
5
+ audience: Audience;
6
+ nonce?: string;
7
+ markdownTwinUrl?: string;
8
+ }
9
+ export interface CachePolicyOptions {
10
+ renderMode: RenderMode;
11
+ audience: Audience;
12
+ }
13
+ export declare function buildSharedPublicationHeaders(options: HeaderPolicyOptions): Record<string, string>;
14
+ export declare function buildContentSecurityPolicy(nonce?: string): string;
15
+ export declare function buildCacheControl({ renderMode, audience }: CachePolicyOptions): string;
16
+ export declare function normalizeHeaders(headers: HeaderMap | undefined): Record<string, string>;
17
+ export declare function isPrivateAudience(audience: Audience): boolean;
18
+ //# sourceMappingURL=policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAElE,eAAO,MAAM,yBAAyB,yJAO5B,CAAA;AAEV,MAAM,WAAW,mBAAmB;IACnC,UAAU,EAAE,UAAU,CAAA;IACtB,QAAQ,EAAE,QAAQ,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,eAAe,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,kBAAkB;IAClC,UAAU,EAAE,UAAU,CAAA;IACtB,QAAQ,EAAE,QAAQ,CAAA;CAClB;AAED,wBAAgB,6BAA6B,CAC5C,OAAO,EAAE,mBAAmB,GAC1B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAiBxB;AAED,wBAAgB,0BAA0B,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAoBjE;AAED,wBAAgB,iBAAiB,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,kBAAkB,GAAG,MAAM,CAMtF;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAavF;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAO7D"}
package/dist/policy.js ADDED
@@ -0,0 +1,72 @@
1
+ export const REQUIRED_SECURITY_HEADERS = [
2
+ 'content-security-policy',
3
+ 'strict-transport-security',
4
+ 'x-frame-options',
5
+ 'x-content-type-options',
6
+ 'referrer-policy',
7
+ 'permissions-policy',
8
+ ];
9
+ export function buildSharedPublicationHeaders(options) {
10
+ const headers = {
11
+ 'Content-Security-Policy': buildContentSecurityPolicy(options.nonce),
12
+ 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',
13
+ 'X-Frame-Options': 'DENY',
14
+ 'X-Content-Type-Options': 'nosniff',
15
+ 'Referrer-Policy': 'strict-origin-when-cross-origin',
16
+ 'Permissions-Policy': 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), microphone=(), payment=(), usb=()',
17
+ 'Cache-Control': buildCacheControl(options),
18
+ };
19
+ if (options.markdownTwinUrl) {
20
+ headers.Link = `<${options.markdownTwinUrl}>; rel="alternate"; type="text/markdown"`;
21
+ }
22
+ return headers;
23
+ }
24
+ export function buildContentSecurityPolicy(nonce) {
25
+ const scriptSources = ["'self'"];
26
+ const styleSources = ["'self'"];
27
+ if (nonce) {
28
+ scriptSources.push(`'nonce-${nonce}'`);
29
+ styleSources.push(`'nonce-${nonce}'`);
30
+ }
31
+ return [
32
+ "default-src 'self'",
33
+ `script-src ${scriptSources.join(' ')}`,
34
+ `style-src ${styleSources.join(' ')}`,
35
+ "img-src 'self' data: https:",
36
+ "font-src 'self' data:",
37
+ "connect-src 'self' https:",
38
+ "object-src 'none'",
39
+ "base-uri 'self'",
40
+ "frame-ancestors 'none'",
41
+ 'upgrade-insecure-requests',
42
+ ].join('; ');
43
+ }
44
+ export function buildCacheControl({ renderMode, audience }) {
45
+ if (isPrivateAudience(audience))
46
+ return 'private, no-store';
47
+ if (renderMode === 'static') {
48
+ return 'public, max-age=300, s-maxage=86400, stale-while-revalidate=604800';
49
+ }
50
+ return 'public, max-age=0, s-maxage=300, stale-while-revalidate=86400';
51
+ }
52
+ export function normalizeHeaders(headers) {
53
+ const normalized = {};
54
+ if (!headers)
55
+ return normalized;
56
+ for (const [name, value] of Object.entries(headers)) {
57
+ if (typeof value === 'string') {
58
+ normalized[name.toLowerCase()] = value;
59
+ }
60
+ else if (Array.isArray(value)) {
61
+ normalized[name.toLowerCase()] = value.join(', ');
62
+ }
63
+ }
64
+ return normalized;
65
+ }
66
+ export function isPrivateAudience(audience) {
67
+ return (audience === 'subscriber' ||
68
+ audience === 'member' ||
69
+ audience === 'admin' ||
70
+ audience === 'private');
71
+ }
72
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACxC,yBAAyB;IACzB,2BAA2B;IAC3B,iBAAiB;IACjB,wBAAwB;IACxB,iBAAiB;IACjB,oBAAoB;CACX,CAAA;AAcV,MAAM,UAAU,6BAA6B,CAC5C,OAA4B;IAE5B,MAAM,OAAO,GAA2B;QACvC,yBAAyB,EAAE,0BAA0B,CAAC,OAAO,CAAC,KAAK,CAAC;QACpE,2BAA2B,EAAE,8CAA8C;QAC3E,iBAAiB,EAAE,MAAM;QACzB,wBAAwB,EAAE,SAAS;QACnC,iBAAiB,EAAE,iCAAiC;QACpD,oBAAoB,EACnB,8FAA8F;QAC/F,eAAe,EAAE,iBAAiB,CAAC,OAAO,CAAC;KAC3C,CAAA;IAED,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;QAC7B,OAAO,CAAC,IAAI,GAAG,IAAI,OAAO,CAAC,eAAe,0CAA0C,CAAA;IACrF,CAAC;IAED,OAAO,OAAO,CAAA;AACf,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,KAAc;IACxD,MAAM,aAAa,GAAG,CAAC,QAAQ,CAAC,CAAA;IAChC,MAAM,YAAY,GAAG,CAAC,QAAQ,CAAC,CAAA;IAC/B,IAAI,KAAK,EAAE,CAAC;QACX,aAAa,CAAC,IAAI,CAAC,UAAU,KAAK,GAAG,CAAC,CAAA;QACtC,YAAY,CAAC,IAAI,CAAC,UAAU,KAAK,GAAG,CAAC,CAAA;IACtC,CAAC;IAED,OAAO;QACN,oBAAoB;QACpB,cAAc,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;QACvC,aAAa,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;QACrC,6BAA6B;QAC7B,uBAAuB;QACvB,2BAA2B;QAC3B,mBAAmB;QACnB,iBAAiB;QACjB,wBAAwB;QACxB,2BAA2B;KAC3B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACb,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAsB;IAC7E,IAAI,iBAAiB,CAAC,QAAQ,CAAC;QAAE,OAAO,mBAAmB,CAAA;IAC3D,IAAI,UAAU,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,oEAAoE,CAAA;IAC5E,CAAC;IACD,OAAO,+DAA+D,CAAA;AACvE,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAA8B;IAC9D,MAAM,UAAU,GAA2B,EAAE,CAAA;IAC7C,IAAI,CAAC,OAAO;QAAE,OAAO,UAAU,CAAA;IAE/B,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACrD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC/B,UAAU,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,GAAG,KAAK,CAAA;QACvC,CAAC;aAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACjC,UAAU,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAClD,CAAC;IACF,CAAC;IAED,OAAO,UAAU,CAAA;AAClB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,QAAkB;IACnD,OAAO,CACN,QAAQ,KAAK,YAAY;QACzB,QAAQ,KAAK,QAAQ;QACrB,QAAQ,KAAK,OAAO;QACpB,QAAQ,KAAK,SAAS,CACtB,CAAA;AACF,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { type ConformanceFixtureInput, type FleetEvidenceBundle, type SiteEvidenceBundle } from './schema.js';
2
+ export interface RunConformanceOptions {
3
+ now?: () => string;
4
+ }
5
+ export declare function runSiteConformance(input: ConformanceFixtureInput, options?: RunConformanceOptions): SiteEvidenceBundle;
6
+ export declare function runFleetConformance(inputs: ConformanceFixtureInput[], options?: RunConformanceOptions): FleetEvidenceBundle;
7
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAEA,OAAO,EAKN,KAAK,uBAAuB,EAG5B,KAAK,mBAAmB,EAExB,KAAK,kBAAkB,EAEvB,MAAM,aAAa,CAAA;AAKpB,MAAM,WAAW,qBAAqB;IACrC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAClB;AAED,wBAAgB,kBAAkB,CACjC,KAAK,EAAE,uBAAuB,EAC9B,OAAO,GAAE,qBAA0B,GACjC,kBAAkB,CA6BpB;AAED,wBAAgB,mBAAmB,CAClC,MAAM,EAAE,uBAAuB,EAAE,EACjC,OAAO,GAAE,qBAA0B,GACjC,mBAAmB,CAuBrB"}