@growth-labs/conformance 0.1.0 → 0.2.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 +23 -0
- package/README.md +51 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/policy.d.ts +21 -2
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +48 -10
- package/dist/policy.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +512 -15
- package/dist/runner.js.map +1 -1
- package/dist/schema.d.ts +503 -12
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +42 -1
- package/dist/schema.js.map +1 -1
- package/fixtures/negative/required-negative-fixtures.json +131 -0
- package/fixtures/valid/public-article.json +56 -2
- package/package.json +1 -1
- package/src/index.ts +13 -0
- package/src/policy.ts +70 -11
- package/src/runner.ts +584 -14
- package/src/schema.ts +49 -1
package/dist/schema.js
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
export const EVIDENCE_SCHEMA_VERSION = 'growth-labs.conformance.
|
|
2
|
+
export const EVIDENCE_SCHEMA_VERSION = 'growth-labs.conformance.v2';
|
|
3
|
+
export const REQUIRED_FUNCTIONAL_PROBE_NAMES = [
|
|
4
|
+
'oauth',
|
|
5
|
+
'media',
|
|
6
|
+
'images',
|
|
7
|
+
'analytics',
|
|
8
|
+
'embeds',
|
|
9
|
+
'error-pages',
|
|
10
|
+
];
|
|
3
11
|
export const REQUIRED_NEGATIVE_FIXTURE_CODES = {
|
|
4
12
|
'csp-unsafe-inline': 'security.csp.unsafe_inline',
|
|
13
|
+
'missing-coop': 'security.header.missing',
|
|
14
|
+
'missing-corp': 'security.header.missing',
|
|
15
|
+
'hsts-below-one-year': 'security.hsts.max_age_too_low',
|
|
16
|
+
'csp-wildcard-default-src': 'security.csp.default_src_wildcard',
|
|
17
|
+
'csp-unsafe-eval': 'security.csp.unsafe_eval',
|
|
18
|
+
'csp-inline-executable-without-nonce-or-hash': 'security.csp.inline_executable_without_nonce_or_hash',
|
|
19
|
+
'conflicting-duplicate-security-header': 'security.header.conflicting_values',
|
|
20
|
+
'missing-functional-probe-receipt': 'security.functional_probe.missing',
|
|
21
|
+
'missing-csp-report-receipt': 'security.csp_report.missing',
|
|
22
|
+
'failed-csp-report-receipt': 'security.csp_report.failed',
|
|
5
23
|
'headers-static-only-while-ssr-missing': 'security.headers.static_only_for_ssr',
|
|
6
24
|
'undeclared-na': 'publication.na.undeclared',
|
|
7
25
|
'stringified-is-accessible-for-free': 'structured_data.is_accessible_for_free.stringified',
|
|
@@ -50,6 +68,27 @@ export const cacheVariantSchema = z.object({
|
|
|
50
68
|
vary: z.array(z.string().min(1)).default([]),
|
|
51
69
|
headers: headerMapSchema.default({}),
|
|
52
70
|
});
|
|
71
|
+
export const securityFunctionalProbeReceiptSchema = z.object({
|
|
72
|
+
name: z.string().min(1),
|
|
73
|
+
status: checkStatusSchema,
|
|
74
|
+
checkedAt: z.string().min(1).optional(),
|
|
75
|
+
evidence: z.string().min(1).optional(),
|
|
76
|
+
capabilityBoundary: z.string().min(1).optional(),
|
|
77
|
+
});
|
|
78
|
+
export const cspReportReceiptSchema = z.object({
|
|
79
|
+
name: z.string().min(1),
|
|
80
|
+
status: checkStatusSchema,
|
|
81
|
+
checkedAt: z.string().min(1).optional(),
|
|
82
|
+
evidence: z.string().min(1).optional(),
|
|
83
|
+
endpoint: z.string().min(1).optional(),
|
|
84
|
+
capabilityBoundary: z.string().min(1).optional(),
|
|
85
|
+
});
|
|
86
|
+
export const securityReceiptsSchema = z
|
|
87
|
+
.object({
|
|
88
|
+
functionalProbes: z.array(securityFunctionalProbeReceiptSchema).default([]),
|
|
89
|
+
cspReports: z.array(cspReportReceiptSchema).default([]),
|
|
90
|
+
})
|
|
91
|
+
.default({ functionalProbes: [], cspReports: [] });
|
|
53
92
|
export const buildEvidenceSchema = z.object({
|
|
54
93
|
commitSha: z.string().min(7).optional(),
|
|
55
94
|
artifactId: z.string().min(1).optional(),
|
|
@@ -74,6 +113,7 @@ export const conformanceFixtureSchema = z.object({
|
|
|
74
113
|
expected: conformanceExpectedSchema.default({}),
|
|
75
114
|
publicationSurfaces: z.array(publicationSurfaceSchema).default([]),
|
|
76
115
|
cacheVariants: z.array(cacheVariantSchema).default([]),
|
|
116
|
+
securityReceipts: securityReceiptsSchema,
|
|
77
117
|
buildEvidence: buildEvidenceSchema.optional(),
|
|
78
118
|
expectedFailingCode: z.string().min(1).optional(),
|
|
79
119
|
});
|
|
@@ -107,6 +147,7 @@ export const siteEvidenceBundleSchema = z.object({
|
|
|
107
147
|
summary: evidenceSummarySchema,
|
|
108
148
|
checks: z.array(conformanceCheckReceiptSchema),
|
|
109
149
|
findings: z.array(conformanceFindingSchema),
|
|
150
|
+
securityReceipts: securityReceiptsSchema,
|
|
110
151
|
});
|
|
111
152
|
export const fleetEvidenceBundleSchema = z.object({
|
|
112
153
|
schemaVersion: z.literal(EVIDENCE_SCHEMA_VERSION),
|
package/dist/schema.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,MAAM,CAAC,MAAM,uBAAuB,GAAG,4BAA4B,CAAA;
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,6 +10,137 @@
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
|
+
{
|
|
14
|
+
"name": "missing-coop",
|
|
15
|
+
"expectedFailingCode": "security.header.missing",
|
|
16
|
+
"replacePaths": ["response.headers"],
|
|
17
|
+
"patch": {
|
|
18
|
+
"response": {
|
|
19
|
+
"headers": {
|
|
20
|
+
"content-security-policy": "default-src 'self'; script-src 'self' 'nonce-fixture'; style-src 'self' 'nonce-fixture'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; upgrade-insecure-requests",
|
|
21
|
+
"strict-transport-security": "max-age=31536000",
|
|
22
|
+
"cross-origin-resource-policy": "same-origin",
|
|
23
|
+
"x-frame-options": "DENY",
|
|
24
|
+
"x-content-type-options": "nosniff",
|
|
25
|
+
"referrer-policy": "strict-origin-when-cross-origin",
|
|
26
|
+
"permissions-policy": "accelerometer=(), camera=(), geolocation=(), gyroscope=(), microphone=(), payment=(), usb=()",
|
|
27
|
+
"cache-control": "public, max-age=0, s-maxage=300, stale-while-revalidate=86400"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "missing-corp",
|
|
34
|
+
"expectedFailingCode": "security.header.missing",
|
|
35
|
+
"replacePaths": ["response.headers"],
|
|
36
|
+
"patch": {
|
|
37
|
+
"response": {
|
|
38
|
+
"headers": {
|
|
39
|
+
"content-security-policy": "default-src 'self'; script-src 'self' 'nonce-fixture'; style-src 'self' 'nonce-fixture'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; upgrade-insecure-requests",
|
|
40
|
+
"strict-transport-security": "max-age=31536000",
|
|
41
|
+
"cross-origin-opener-policy": "same-origin",
|
|
42
|
+
"x-frame-options": "DENY",
|
|
43
|
+
"x-content-type-options": "nosniff",
|
|
44
|
+
"referrer-policy": "strict-origin-when-cross-origin",
|
|
45
|
+
"permissions-policy": "accelerometer=(), camera=(), geolocation=(), gyroscope=(), microphone=(), payment=(), usb=()",
|
|
46
|
+
"cache-control": "public, max-age=0, s-maxage=300, stale-while-revalidate=86400"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "hsts-below-one-year",
|
|
53
|
+
"expectedFailingCode": "security.hsts.max_age_too_low",
|
|
54
|
+
"patch": {
|
|
55
|
+
"response": {
|
|
56
|
+
"headers": {
|
|
57
|
+
"strict-transport-security": "max-age=300"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "csp-wildcard-default-src",
|
|
64
|
+
"expectedFailingCode": "security.csp.default_src_wildcard",
|
|
65
|
+
"patch": {
|
|
66
|
+
"response": {
|
|
67
|
+
"headers": {
|
|
68
|
+
"content-security-policy": "default-src *; script-src 'self' 'nonce-fixture'; style-src 'self' 'nonce-fixture'; img-src 'self' data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "csp-unsafe-eval",
|
|
75
|
+
"expectedFailingCode": "security.csp.unsafe_eval",
|
|
76
|
+
"patch": {
|
|
77
|
+
"response": {
|
|
78
|
+
"headers": {
|
|
79
|
+
"content-security-policy": "default-src 'self'; script-src 'self' 'nonce-fixture' 'unsafe-eval'; style-src 'self' 'nonce-fixture'; img-src 'self' data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "csp-inline-executable-without-nonce-or-hash",
|
|
86
|
+
"expectedFailingCode": "security.csp.inline_executable_without_nonce_or_hash",
|
|
87
|
+
"patch": {
|
|
88
|
+
"response": {
|
|
89
|
+
"headers": {
|
|
90
|
+
"content-security-policy": "default-src 'self'; script-src 'self' 'nonce-fixture'; style-src 'self' 'nonce-fixture'; img-src 'self' data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
|
|
91
|
+
},
|
|
92
|
+
"html": "<!doctype html><html><head><title>Fixture publication conformance article</title><link rel=\"canonical\" href=\"https://fixture.example/articles/valid-publication\"><meta name=\"robots\" content=\"index, follow, max-image-preview:large\"><script>window.fixtureInlineExecutable = true</script><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"NewsArticle\",\"headline\":\"Fixture publication conformance article\",\"isAccessibleForFree\":true,\"datePublished\":\"2026-07-15T12:00:00Z\",\"image\":[{\"url\":\"https://media.fixture.example/images/articles/valid-16x9.jpg\",\"width\":1200,\"height\":675}]}</script></head><body><main><h1>Fixture publication conformance article</h1></main></body></html>"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"name": "conflicting-duplicate-security-header",
|
|
98
|
+
"expectedFailingCode": "security.header.conflicting_values",
|
|
99
|
+
"patch": {
|
|
100
|
+
"response": {
|
|
101
|
+
"headers": {
|
|
102
|
+
"x-content-type-options": ["nosniff", "none"]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"name": "missing-functional-probe-receipt",
|
|
109
|
+
"expectedFailingCode": "security.functional_probe.missing",
|
|
110
|
+
"replacePaths": ["securityReceipts.functionalProbes"],
|
|
111
|
+
"patch": {
|
|
112
|
+
"securityReceipts": {
|
|
113
|
+
"functionalProbes": []
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"name": "missing-csp-report-receipt",
|
|
119
|
+
"expectedFailingCode": "security.csp_report.missing",
|
|
120
|
+
"replacePaths": ["securityReceipts.cspReports"],
|
|
121
|
+
"patch": {
|
|
122
|
+
"securityReceipts": {
|
|
123
|
+
"cspReports": []
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"name": "failed-csp-report-receipt",
|
|
129
|
+
"expectedFailingCode": "security.csp_report.failed",
|
|
130
|
+
"replacePaths": ["securityReceipts.cspReports"],
|
|
131
|
+
"patch": {
|
|
132
|
+
"securityReceipts": {
|
|
133
|
+
"cspReports": [
|
|
134
|
+
{
|
|
135
|
+
"name": "csp-report-canary",
|
|
136
|
+
"status": "fail",
|
|
137
|
+
"checkedAt": "2026-07-15T23:30:00Z",
|
|
138
|
+
"evidence": "Synthetic blocked-script canary report was not observed by the consumer-owned reporting endpoint."
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
13
144
|
{
|
|
14
145
|
"name": "headers-static-only-while-ssr-missing",
|
|
15
146
|
"expectedFailingCode": "security.headers.static_only_for_ssr",
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"url": "https://fixture.example/articles/valid-publication",
|
|
11
11
|
"headers": {
|
|
12
12
|
"content-security-policy": "default-src 'self'; script-src 'self' 'nonce-fixture'; style-src 'self' 'nonce-fixture'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; upgrade-insecure-requests",
|
|
13
|
-
"strict-transport-security": "max-age=31536000
|
|
13
|
+
"strict-transport-security": "max-age=31536000",
|
|
14
|
+
"cross-origin-opener-policy": "same-origin",
|
|
15
|
+
"cross-origin-resource-policy": "same-origin",
|
|
14
16
|
"x-frame-options": "DENY",
|
|
15
17
|
"x-content-type-options": "nosniff",
|
|
16
18
|
"referrer-policy": "strict-origin-when-cross-origin",
|
|
@@ -62,12 +64,64 @@
|
|
|
62
64
|
"url": "https://fixture.example/articles/valid-publication"
|
|
63
65
|
}
|
|
64
66
|
],
|
|
67
|
+
"securityReceipts": {
|
|
68
|
+
"functionalProbes": [
|
|
69
|
+
{
|
|
70
|
+
"name": "oauth",
|
|
71
|
+
"status": "na",
|
|
72
|
+
"checkedAt": "2026-07-15T23:30:00Z",
|
|
73
|
+
"capabilityBoundary": "Fixture site has no OAuth flow",
|
|
74
|
+
"evidence": "Fixture-only package conformance baseline"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "media",
|
|
78
|
+
"status": "pass",
|
|
79
|
+
"checkedAt": "2026-07-15T23:30:00Z",
|
|
80
|
+
"evidence": "Fixture media image probes loaded under enforced CSP"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "images",
|
|
84
|
+
"status": "pass",
|
|
85
|
+
"checkedAt": "2026-07-15T23:30:00Z",
|
|
86
|
+
"evidence": "Fixture structured and rendered images loaded under enforced CSP"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"name": "analytics",
|
|
90
|
+
"status": "na",
|
|
91
|
+
"checkedAt": "2026-07-15T23:30:00Z",
|
|
92
|
+
"capabilityBoundary": "Fixture site has no analytics runtime",
|
|
93
|
+
"evidence": "Fixture-only package conformance baseline"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"name": "embeds",
|
|
97
|
+
"status": "na",
|
|
98
|
+
"checkedAt": "2026-07-15T23:30:00Z",
|
|
99
|
+
"capabilityBoundary": "Fixture article has no third-party embeds",
|
|
100
|
+
"evidence": "Fixture-only package conformance baseline"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "error-pages",
|
|
104
|
+
"status": "pass",
|
|
105
|
+
"checkedAt": "2026-07-15T23:30:00Z",
|
|
106
|
+
"evidence": "Fixture error-page probe returned enforced security headers"
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
"cspReports": [
|
|
110
|
+
{
|
|
111
|
+
"name": "csp-report-canary",
|
|
112
|
+
"status": "pass",
|
|
113
|
+
"checkedAt": "2026-07-15T23:30:00Z",
|
|
114
|
+
"endpoint": "fixture-csp-report-endpoint",
|
|
115
|
+
"evidence": "Synthetic blocked-script canary produced a sanitized CSP report receipt"
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
},
|
|
65
119
|
"buildEvidence": {
|
|
66
120
|
"commitSha": "bd39d718f6a5b0df9a249e11638a8b430050941e",
|
|
67
121
|
"artifactId": "fixture-build-001",
|
|
68
122
|
"generatedAt": "2026-07-15T23:30:00Z",
|
|
69
123
|
"packageVersions": {
|
|
70
|
-
"@growth-labs/conformance": "0.
|
|
124
|
+
"@growth-labs/conformance": "0.2.0",
|
|
71
125
|
"@growth-labs/seo": "0.9.0"
|
|
72
126
|
},
|
|
73
127
|
"staticHeaderPolicy": true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@growth-labs/conformance",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Reusable fleet publication conformance harness for Growth Labs sites: headers, cache, metadata, structured data, images, publication surfaces, and evidence bundles.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
package/src/index.ts
CHANGED
|
@@ -2,11 +2,17 @@ export {
|
|
|
2
2
|
buildCacheControl,
|
|
3
3
|
buildContentSecurityPolicy,
|
|
4
4
|
buildSharedPublicationHeaders,
|
|
5
|
+
buildStrictTransportSecurity,
|
|
5
6
|
type CachePolicyOptions,
|
|
7
|
+
type ContentSecurityPolicyOptions,
|
|
8
|
+
type CrossOriginOpenerPolicy,
|
|
9
|
+
type CrossOriginResourcePolicy,
|
|
10
|
+
collectHeaders,
|
|
6
11
|
type HeaderPolicyOptions,
|
|
7
12
|
isPrivateAudience,
|
|
8
13
|
normalizeHeaders,
|
|
9
14
|
REQUIRED_SECURITY_HEADERS,
|
|
15
|
+
type StrictTransportSecurityOptions,
|
|
10
16
|
} from './policy.js'
|
|
11
17
|
export { type RunConformanceOptions, runFleetConformance, runSiteConformance } from './runner.js'
|
|
12
18
|
export {
|
|
@@ -19,12 +25,14 @@ export {
|
|
|
19
25
|
type ConformanceFinding,
|
|
20
26
|
type ConformanceFixture,
|
|
21
27
|
type ConformanceFixtureInput,
|
|
28
|
+
type CspReportReceipt,
|
|
22
29
|
cacheVariantSchema,
|
|
23
30
|
checkStatusSchema,
|
|
24
31
|
conformanceCheckReceiptSchema,
|
|
25
32
|
conformanceExpectedSchema,
|
|
26
33
|
conformanceFindingSchema,
|
|
27
34
|
conformanceFixtureSchema,
|
|
35
|
+
cspReportReceiptSchema,
|
|
28
36
|
EVIDENCE_SCHEMA_VERSION,
|
|
29
37
|
type EvidenceSummary,
|
|
30
38
|
evidenceSummarySchema,
|
|
@@ -37,13 +45,18 @@ export {
|
|
|
37
45
|
type PublicationSurfaceEvidence,
|
|
38
46
|
parseConformanceFixture,
|
|
39
47
|
publicationSurfaceSchema,
|
|
48
|
+
REQUIRED_FUNCTIONAL_PROBE_NAMES,
|
|
40
49
|
REQUIRED_NEGATIVE_FIXTURE_CODES,
|
|
41
50
|
type RenderMode,
|
|
42
51
|
type RequiredNegativeFindingCode,
|
|
43
52
|
type RequiredNegativeFixtureId,
|
|
44
53
|
renderModeSchema,
|
|
54
|
+
type SecurityFunctionalProbeReceipt,
|
|
55
|
+
type SecurityReceipts,
|
|
45
56
|
type SiteEvidenceBundle,
|
|
46
57
|
type SurfaceResponse,
|
|
58
|
+
securityFunctionalProbeReceiptSchema,
|
|
59
|
+
securityReceiptsSchema,
|
|
47
60
|
siteEvidenceBundleSchema,
|
|
48
61
|
surfaceResponseSchema,
|
|
49
62
|
validateFleetEvidenceBundle,
|
package/src/policy.ts
CHANGED
|
@@ -3,16 +3,38 @@ import type { Audience, HeaderMap, RenderMode } from './schema.js'
|
|
|
3
3
|
export const REQUIRED_SECURITY_HEADERS = [
|
|
4
4
|
'content-security-policy',
|
|
5
5
|
'strict-transport-security',
|
|
6
|
+
'cross-origin-opener-policy',
|
|
7
|
+
'cross-origin-resource-policy',
|
|
6
8
|
'x-frame-options',
|
|
7
9
|
'x-content-type-options',
|
|
8
10
|
'referrer-policy',
|
|
9
11
|
'permissions-policy',
|
|
10
12
|
] as const
|
|
11
13
|
|
|
14
|
+
export type CrossOriginOpenerPolicy = 'same-origin' | 'same-origin-allow-popups' | 'unsafe-none'
|
|
15
|
+
export type CrossOriginResourcePolicy = 'same-origin' | 'same-site' | 'cross-origin'
|
|
16
|
+
|
|
17
|
+
export interface StrictTransportSecurityOptions {
|
|
18
|
+
maxAgeSeconds?: number
|
|
19
|
+
includeSubDomains?: boolean
|
|
20
|
+
preload?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ContentSecurityPolicyOptions {
|
|
24
|
+
nonce?: string
|
|
25
|
+
reportUri?: string
|
|
26
|
+
reportTo?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
export interface HeaderPolicyOptions {
|
|
13
30
|
renderMode: RenderMode
|
|
14
31
|
audience: Audience
|
|
15
32
|
nonce?: string
|
|
33
|
+
cspReportUri?: string
|
|
34
|
+
cspReportTo?: string
|
|
35
|
+
strictTransportSecurity?: StrictTransportSecurityOptions
|
|
36
|
+
crossOriginOpenerPolicy?: CrossOriginOpenerPolicy
|
|
37
|
+
crossOriginResourcePolicy?: CrossOriginResourcePolicy
|
|
16
38
|
markdownTwinUrl?: string
|
|
17
39
|
}
|
|
18
40
|
|
|
@@ -25,8 +47,14 @@ export function buildSharedPublicationHeaders(
|
|
|
25
47
|
options: HeaderPolicyOptions,
|
|
26
48
|
): Record<string, string> {
|
|
27
49
|
const headers: Record<string, string> = {
|
|
28
|
-
'Content-Security-Policy': buildContentSecurityPolicy(
|
|
29
|
-
|
|
50
|
+
'Content-Security-Policy': buildContentSecurityPolicy({
|
|
51
|
+
nonce: options.nonce,
|
|
52
|
+
reportUri: options.cspReportUri,
|
|
53
|
+
reportTo: options.cspReportTo,
|
|
54
|
+
}),
|
|
55
|
+
'Strict-Transport-Security': buildStrictTransportSecurity(options.strictTransportSecurity),
|
|
56
|
+
'Cross-Origin-Opener-Policy': options.crossOriginOpenerPolicy ?? 'same-origin',
|
|
57
|
+
'Cross-Origin-Resource-Policy': options.crossOriginResourcePolicy ?? 'same-origin',
|
|
30
58
|
'X-Frame-Options': 'DENY',
|
|
31
59
|
'X-Content-Type-Options': 'nosniff',
|
|
32
60
|
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
@@ -42,15 +70,16 @@ export function buildSharedPublicationHeaders(
|
|
|
42
70
|
return headers
|
|
43
71
|
}
|
|
44
72
|
|
|
45
|
-
export function buildContentSecurityPolicy(
|
|
73
|
+
export function buildContentSecurityPolicy(input?: string | ContentSecurityPolicyOptions): string {
|
|
74
|
+
const options = typeof input === 'string' ? { nonce: input } : (input ?? {})
|
|
46
75
|
const scriptSources = ["'self'"]
|
|
47
76
|
const styleSources = ["'self'"]
|
|
48
|
-
if (nonce) {
|
|
49
|
-
scriptSources.push(`'nonce-${nonce}'`)
|
|
50
|
-
styleSources.push(`'nonce-${nonce}'`)
|
|
77
|
+
if (options.nonce) {
|
|
78
|
+
scriptSources.push(`'nonce-${options.nonce}'`)
|
|
79
|
+
styleSources.push(`'nonce-${options.nonce}'`)
|
|
51
80
|
}
|
|
52
81
|
|
|
53
|
-
|
|
82
|
+
const directives = [
|
|
54
83
|
"default-src 'self'",
|
|
55
84
|
`script-src ${scriptSources.join(' ')}`,
|
|
56
85
|
`style-src ${styleSources.join(' ')}`,
|
|
@@ -61,7 +90,28 @@ export function buildContentSecurityPolicy(nonce?: string): string {
|
|
|
61
90
|
"base-uri 'self'",
|
|
62
91
|
"frame-ancestors 'none'",
|
|
63
92
|
'upgrade-insecure-requests',
|
|
64
|
-
]
|
|
93
|
+
]
|
|
94
|
+
if (options.reportTo) directives.push(`report-to ${options.reportTo}`)
|
|
95
|
+
if (options.reportUri) directives.push(`report-uri ${options.reportUri}`)
|
|
96
|
+
|
|
97
|
+
return directives.join('; ')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function buildStrictTransportSecurity(options: StrictTransportSecurityOptions = {}): string {
|
|
101
|
+
const maxAgeSeconds = options.maxAgeSeconds ?? 31536000
|
|
102
|
+
if (!Number.isInteger(maxAgeSeconds) || maxAgeSeconds < 31536000) {
|
|
103
|
+
throw new Error('Strict-Transport-Security maxAgeSeconds must be at least 31536000')
|
|
104
|
+
}
|
|
105
|
+
if (options.preload === true && options.includeSubDomains !== true) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
'Strict-Transport-Security preload requires includeSubDomains after host inventory proof',
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const directives = [`max-age=${maxAgeSeconds}`]
|
|
112
|
+
if (options.includeSubDomains === true) directives.push('includeSubDomains')
|
|
113
|
+
if (options.preload === true) directives.push('preload')
|
|
114
|
+
return directives.join('; ')
|
|
65
115
|
}
|
|
66
116
|
|
|
67
117
|
export function buildCacheControl({ renderMode, audience }: CachePolicyOptions): string {
|
|
@@ -73,14 +123,23 @@ export function buildCacheControl({ renderMode, audience }: CachePolicyOptions):
|
|
|
73
123
|
}
|
|
74
124
|
|
|
75
125
|
export function normalizeHeaders(headers: HeaderMap | undefined): Record<string, string> {
|
|
126
|
+
const entries = collectHeaders(headers)
|
|
76
127
|
const normalized: Record<string, string> = {}
|
|
77
|
-
|
|
128
|
+
for (const [name, values] of Object.entries(entries)) {
|
|
129
|
+
normalized[name] = values.join(', ')
|
|
130
|
+
}
|
|
131
|
+
return normalized
|
|
132
|
+
}
|
|
78
133
|
|
|
134
|
+
export function collectHeaders(headers: HeaderMap | undefined): Record<string, string[]> {
|
|
135
|
+
const normalized: Record<string, string[]> = {}
|
|
136
|
+
if (!headers) return normalized
|
|
79
137
|
for (const [name, value] of Object.entries(headers)) {
|
|
138
|
+
const normalizedName = name.toLowerCase()
|
|
80
139
|
if (typeof value === 'string') {
|
|
81
|
-
normalized[
|
|
140
|
+
normalized[normalizedName] = [...(normalized[normalizedName] ?? []), value]
|
|
82
141
|
} else if (Array.isArray(value)) {
|
|
83
|
-
normalized[
|
|
142
|
+
normalized[normalizedName] = [...(normalized[normalizedName] ?? []), ...value]
|
|
84
143
|
}
|
|
85
144
|
}
|
|
86
145
|
|