@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 +14 -0
- package/README.md +118 -0
- package/dist/html.d.ts +24 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +138 -0
- package/dist/html.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/policy.d.ts +18 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +72 -0
- package/dist/policy.js.map +1 -0
- package/dist/runner.d.ts +7 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +382 -0
- package/dist/runner.js.map +1 -0
- package/dist/schema.d.ts +878 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +129 -0
- package/dist/schema.js.map +1 -0
- package/fixtures/negative/required-negative-fixtures.json +132 -0
- package/fixtures/valid/public-article.json +76 -0
- package/package.json +48 -0
- package/src/html.ts +175 -0
- package/src/index.ts +51 -0
- package/src/policy.ts +97 -0
- package/src/runner.ts +477 -0
- package/src/schema.ts +165 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const EVIDENCE_SCHEMA_VERSION = 'growth-labs.conformance.v1';
|
|
3
|
+
export const REQUIRED_NEGATIVE_FIXTURE_CODES = {
|
|
4
|
+
'csp-unsafe-inline': 'security.csp.unsafe_inline',
|
|
5
|
+
'headers-static-only-while-ssr-missing': 'security.headers.static_only_for_ssr',
|
|
6
|
+
'undeclared-na': 'publication.na.undeclared',
|
|
7
|
+
'stringified-is-accessible-for-free': 'structured_data.is_accessible_for_free.stringified',
|
|
8
|
+
'video-object-date-only-upload-date': 'structured_data.video_object.upload_date.date_only',
|
|
9
|
+
'missing-image-dimensions': 'images.dimensions.missing',
|
|
10
|
+
'hero-under-1200px': 'images.hero.under_1200',
|
|
11
|
+
'missing-max-image-preview-large': 'metadata.robots.missing_max_image_preview_large',
|
|
12
|
+
'crawler-public-cache-key-collision': 'cache.crawler_public_key_collision',
|
|
13
|
+
'runtime-canonical-override-not-applied': 'metadata.canonical.runtime_override_not_applied',
|
|
14
|
+
};
|
|
15
|
+
export const audienceSchema = z.enum([
|
|
16
|
+
'public',
|
|
17
|
+
'crawler',
|
|
18
|
+
'subscriber',
|
|
19
|
+
'member',
|
|
20
|
+
'admin',
|
|
21
|
+
'private',
|
|
22
|
+
]);
|
|
23
|
+
export const renderModeSchema = z.enum(['ssr', 'static']);
|
|
24
|
+
export const checkStatusSchema = z.enum(['pass', 'fail', 'na']);
|
|
25
|
+
export const findingSeveritySchema = z.enum(['error', 'warning']);
|
|
26
|
+
export const headerValueSchema = z.union([z.string(), z.array(z.string())]);
|
|
27
|
+
export const headerMapSchema = z.record(headerValueSchema);
|
|
28
|
+
export const surfaceResponseSchema = z.object({
|
|
29
|
+
status: z.number().int().min(100).max(599).default(200),
|
|
30
|
+
url: z.string().url().optional(),
|
|
31
|
+
headers: headerMapSchema.default({}),
|
|
32
|
+
html: z.string().optional(),
|
|
33
|
+
body: z.string().optional(),
|
|
34
|
+
});
|
|
35
|
+
export const publicationSurfaceSchema = z.object({
|
|
36
|
+
name: z.string().min(1),
|
|
37
|
+
status: z.enum(['present', 'missing', 'na']),
|
|
38
|
+
url: z.string().url().optional(),
|
|
39
|
+
httpStatus: z.number().int().min(100).max(599).optional(),
|
|
40
|
+
contentType: z.string().min(1).optional(),
|
|
41
|
+
bodyBytes: z.number().int().nonnegative().optional(),
|
|
42
|
+
capabilityBoundary: z.string().min(1).optional(),
|
|
43
|
+
evidence: z.string().min(1).optional(),
|
|
44
|
+
});
|
|
45
|
+
export const cacheVariantSchema = z.object({
|
|
46
|
+
audience: audienceSchema,
|
|
47
|
+
cacheKey: z.string().min(1),
|
|
48
|
+
url: z.string().url().optional(),
|
|
49
|
+
contentClass: z.enum(['html', 'markdown', 'feed', 'asset']).default('html'),
|
|
50
|
+
vary: z.array(z.string().min(1)).default([]),
|
|
51
|
+
headers: headerMapSchema.default({}),
|
|
52
|
+
});
|
|
53
|
+
export const buildEvidenceSchema = z.object({
|
|
54
|
+
commitSha: z.string().min(7).optional(),
|
|
55
|
+
artifactId: z.string().min(1).optional(),
|
|
56
|
+
generatedAt: z.string().min(1).optional(),
|
|
57
|
+
packageVersions: z.record(z.string().min(1)).default({}),
|
|
58
|
+
staticHeaderPolicy: z.boolean().default(false),
|
|
59
|
+
ssrHeaderPolicy: z.boolean().default(false),
|
|
60
|
+
});
|
|
61
|
+
export const conformanceExpectedSchema = z.object({
|
|
62
|
+
canonicalUrl: z.string().url().optional(),
|
|
63
|
+
schemaTypes: z.array(z.string().min(1)).default([]),
|
|
64
|
+
discoverEligible: z.boolean().default(true),
|
|
65
|
+
});
|
|
66
|
+
export const conformanceFixtureSchema = z.object({
|
|
67
|
+
id: z.string().min(1),
|
|
68
|
+
site: z.string().min(1),
|
|
69
|
+
environment: z.string().min(1).default('production'),
|
|
70
|
+
url: z.string().url(),
|
|
71
|
+
renderMode: renderModeSchema,
|
|
72
|
+
audience: audienceSchema,
|
|
73
|
+
response: surfaceResponseSchema,
|
|
74
|
+
expected: conformanceExpectedSchema.default({}),
|
|
75
|
+
publicationSurfaces: z.array(publicationSurfaceSchema).default([]),
|
|
76
|
+
cacheVariants: z.array(cacheVariantSchema).default([]),
|
|
77
|
+
buildEvidence: buildEvidenceSchema.optional(),
|
|
78
|
+
expectedFailingCode: z.string().min(1).optional(),
|
|
79
|
+
});
|
|
80
|
+
export const conformanceFindingSchema = z.object({
|
|
81
|
+
code: z.string().min(1),
|
|
82
|
+
severity: findingSeveritySchema,
|
|
83
|
+
message: z.string().min(1),
|
|
84
|
+
path: z.string().min(1).optional(),
|
|
85
|
+
evidence: z.string().min(1).optional(),
|
|
86
|
+
remediation: z.string().min(1).optional(),
|
|
87
|
+
});
|
|
88
|
+
export const conformanceCheckReceiptSchema = z.object({
|
|
89
|
+
name: z.string().min(1),
|
|
90
|
+
status: checkStatusSchema,
|
|
91
|
+
findings: z.array(conformanceFindingSchema),
|
|
92
|
+
});
|
|
93
|
+
export const evidenceSummarySchema = z.object({
|
|
94
|
+
pass: z.number().int().nonnegative(),
|
|
95
|
+
fail: z.number().int().nonnegative(),
|
|
96
|
+
na: z.number().int().nonnegative(),
|
|
97
|
+
findings: z.number().int().nonnegative(),
|
|
98
|
+
});
|
|
99
|
+
export const siteEvidenceBundleSchema = z.object({
|
|
100
|
+
schemaVersion: z.literal(EVIDENCE_SCHEMA_VERSION),
|
|
101
|
+
generatedAt: z.string().min(1),
|
|
102
|
+
fixtureId: z.string().min(1),
|
|
103
|
+
site: z.string().min(1),
|
|
104
|
+
environment: z.string().min(1),
|
|
105
|
+
url: z.string().url(),
|
|
106
|
+
status: z.enum(['pass', 'fail']),
|
|
107
|
+
summary: evidenceSummarySchema,
|
|
108
|
+
checks: z.array(conformanceCheckReceiptSchema),
|
|
109
|
+
findings: z.array(conformanceFindingSchema),
|
|
110
|
+
});
|
|
111
|
+
export const fleetEvidenceBundleSchema = z.object({
|
|
112
|
+
schemaVersion: z.literal(EVIDENCE_SCHEMA_VERSION),
|
|
113
|
+
generatedAt: z.string().min(1),
|
|
114
|
+
status: z.enum(['pass', 'fail']),
|
|
115
|
+
summary: evidenceSummarySchema.extend({
|
|
116
|
+
sites: z.number().int().nonnegative(),
|
|
117
|
+
}),
|
|
118
|
+
sites: z.array(siteEvidenceBundleSchema),
|
|
119
|
+
});
|
|
120
|
+
export function parseConformanceFixture(input) {
|
|
121
|
+
return conformanceFixtureSchema.parse(input);
|
|
122
|
+
}
|
|
123
|
+
export function validateSiteEvidenceBundle(input) {
|
|
124
|
+
return siteEvidenceBundleSchema.parse(input);
|
|
125
|
+
}
|
|
126
|
+
export function validateFleetEvidenceBundle(input) {
|
|
127
|
+
return fleetEvidenceBundleSchema.parse(input);
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +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;AAEnE,MAAM,CAAC,MAAM,+BAA+B,GAAG;IAC9C,mBAAmB,EAAE,4BAA4B;IACjD,uCAAuC,EAAE,sCAAsC;IAC/E,eAAe,EAAE,2BAA2B;IAC5C,oCAAoC,EAAE,oDAAoD;IAC1F,oCAAoC,EAAE,oDAAoD;IAC1F,0BAA0B,EAAE,2BAA2B;IACvD,mBAAmB,EAAE,wBAAwB;IAC7C,iCAAiC,EAAE,iDAAiD;IACpF,oCAAoC,EAAE,oCAAoC;IAC1E,wCAAwC,EAAE,iDAAiD;CAClF,CAAA;AAMV,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,IAAI,CAAC;IACpC,QAAQ;IACR,SAAS;IACT,YAAY;IACZ,QAAQ;IACR,OAAO;IACP,SAAS;CACT,CAAC,CAAA;AACF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAA;AACzD,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC,CAAA;AAC/D,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAA;AAEjE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAA;AAC3E,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAA;AAE1D,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC;IACvD,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IAChC,OAAO,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC;IACpC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC3B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC3B,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChD,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;IAC5C,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IAChC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;IACzD,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACzC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAE;IACpD,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IAChD,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CACtC,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1C,QAAQ,EAAE,cAAc;IACxB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3B,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IAChC,YAAY,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;IAC3E,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC5C,OAAO,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC;CACpC,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACvC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACxC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACzC,eAAe,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACxD,kBAAkB,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IAC9C,eAAe,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;CAC3C,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,MAAM,CAAC;IACjD,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IACzC,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACnD,gBAAgB,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;CAC3C,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChD,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACrB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC;IACpD,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IACrB,UAAU,EAAE,gBAAgB;IAC5B,QAAQ,EAAE,cAAc;IACxB,QAAQ,EAAE,qBAAqB;IAC/B,QAAQ,EAAE,yBAAyB,CAAC,OAAO,CAAC,EAAE,CAAC;IAC/C,mBAAmB,EAAE,CAAC,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAClE,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACtD,aAAa,EAAE,mBAAmB,CAAC,QAAQ,EAAE;IAC7C,mBAAmB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CACjD,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChD,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,QAAQ,EAAE,qBAAqB;IAC/B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IAClC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACtC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CACzC,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC,MAAM,CAAC;IACrD,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,MAAM,EAAE,iBAAiB;IACzB,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,wBAAwB,CAAC;CAC3C,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IACpC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IACpC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAClC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;CACxC,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChD,aAAa,EAAE,CAAC,CAAC,OAAO,CAAC,uBAAuB,CAAC;IACjD,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IACrB,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,EAAE,qBAAqB;IAC9B,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,6BAA6B,CAAC;IAC9C,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,wBAAwB,CAAC;CAC3C,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,MAAM,CAAC;IACjD,aAAa,EAAE,CAAC,CAAC,OAAO,CAAC,uBAAuB,CAAC;IACjD,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,EAAE,qBAAqB,CAAC,MAAM,CAAC;QACrC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;KACrC,CAAC;IACF,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,wBAAwB,CAAC;CACxC,CAAC,CAAA;AAiBF,MAAM,UAAU,uBAAuB,CAAC,KAAc;IACrD,OAAO,wBAAwB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;AAC7C,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,KAAc;IACxD,OAAO,wBAAwB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;AAC7C,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,KAAc;IACzD,OAAO,yBAAyB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;AAC9C,CAAC"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "csp-unsafe-inline",
|
|
4
|
+
"expectedFailingCode": "security.csp.unsafe_inline",
|
|
5
|
+
"patch": {
|
|
6
|
+
"response": {
|
|
7
|
+
"headers": {
|
|
8
|
+
"content-security-policy": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "headers-static-only-while-ssr-missing",
|
|
15
|
+
"expectedFailingCode": "security.headers.static_only_for_ssr",
|
|
16
|
+
"replacePaths": ["response.headers"],
|
|
17
|
+
"patch": {
|
|
18
|
+
"response": {
|
|
19
|
+
"headers": {
|
|
20
|
+
"cache-control": "public, max-age=0, s-maxage=300"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"buildEvidence": {
|
|
24
|
+
"staticHeaderPolicy": true,
|
|
25
|
+
"ssrHeaderPolicy": false
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "undeclared-na",
|
|
31
|
+
"expectedFailingCode": "publication.na.undeclared",
|
|
32
|
+
"patch": {
|
|
33
|
+
"publicationSurfaces": [
|
|
34
|
+
{
|
|
35
|
+
"name": "sitemap",
|
|
36
|
+
"status": "present",
|
|
37
|
+
"url": "https://fixture.example/sitemap.xml",
|
|
38
|
+
"httpStatus": 200,
|
|
39
|
+
"contentType": "application/xml",
|
|
40
|
+
"bodyBytes": 640
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "robots",
|
|
44
|
+
"status": "present",
|
|
45
|
+
"url": "https://fixture.example/robots.txt",
|
|
46
|
+
"httpStatus": 200,
|
|
47
|
+
"contentType": "text/plain",
|
|
48
|
+
"bodyBytes": 180
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "feed",
|
|
52
|
+
"status": "na"
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "stringified-is-accessible-for-free",
|
|
59
|
+
"expectedFailingCode": "structured_data.is_accessible_for_free.stringified",
|
|
60
|
+
"patch": {
|
|
61
|
+
"response": {
|
|
62
|
+
"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 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>"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"name": "video-object-date-only-upload-date",
|
|
68
|
+
"expectedFailingCode": "structured_data.video_object.upload_date.date_only",
|
|
69
|
+
"patch": {
|
|
70
|
+
"expected": {
|
|
71
|
+
"schemaTypes": ["VideoObject"]
|
|
72
|
+
},
|
|
73
|
+
"response": {
|
|
74
|
+
"html": "<!doctype html><html><head><title>Fixture publication conformance video</title><link rel=\"canonical\" href=\"https://fixture.example/articles/valid-publication\"><meta name=\"robots\" content=\"index, follow, max-image-preview:large\"><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"VideoObject\",\"name\":\"Fixture publication conformance video\",\"isAccessibleForFree\":true,\"uploadDate\":\"2026-07-15\",\"thumbnailUrl\":\"https://media.fixture.example/images/articles/video-poster.jpg\",\"image\":[{\"url\":\"https://media.fixture.example/images/articles/video-poster.jpg\",\"width\":1200,\"height\":675}]}</script></head><body><main><h1>Fixture publication conformance video</h1></main></body></html>"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "missing-image-dimensions",
|
|
80
|
+
"expectedFailingCode": "images.dimensions.missing",
|
|
81
|
+
"patch": {
|
|
82
|
+
"response": {
|
|
83
|
+
"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 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/no-dimensions.jpg\"}]}</script></head><body><main><h1>Fixture publication conformance article</h1></main></body></html>"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "hero-under-1200px",
|
|
89
|
+
"expectedFailingCode": "images.hero.under_1200",
|
|
90
|
+
"patch": {
|
|
91
|
+
"response": {
|
|
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 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/small-hero.jpg\",\"width\":900,\"height\":506}]}</script></head><body><main><h1>Fixture publication conformance article</h1></main></body></html>"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"name": "missing-max-image-preview-large",
|
|
98
|
+
"expectedFailingCode": "metadata.robots.missing_max_image_preview_large",
|
|
99
|
+
"patch": {
|
|
100
|
+
"response": {
|
|
101
|
+
"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\"><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>"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"name": "crawler-public-cache-key-collision",
|
|
107
|
+
"expectedFailingCode": "cache.crawler_public_key_collision",
|
|
108
|
+
"patch": {
|
|
109
|
+
"cacheVariants": [
|
|
110
|
+
{
|
|
111
|
+
"audience": "public",
|
|
112
|
+
"cacheKey": "fixture-news:/articles/valid-publication",
|
|
113
|
+
"url": "https://fixture.example/articles/valid-publication"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"audience": "crawler",
|
|
117
|
+
"cacheKey": "fixture-news:/articles/valid-publication",
|
|
118
|
+
"url": "https://fixture.example/articles/valid-publication"
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"name": "runtime-canonical-override-not-applied",
|
|
125
|
+
"expectedFailingCode": "metadata.canonical.runtime_override_not_applied",
|
|
126
|
+
"patch": {
|
|
127
|
+
"expected": {
|
|
128
|
+
"canonicalUrl": "https://fixture.example/articles/runtime-canonical"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "valid-public-article",
|
|
3
|
+
"site": "fixture-news",
|
|
4
|
+
"environment": "fixture",
|
|
5
|
+
"url": "https://fixture.example/articles/valid-publication",
|
|
6
|
+
"renderMode": "ssr",
|
|
7
|
+
"audience": "public",
|
|
8
|
+
"response": {
|
|
9
|
+
"status": 200,
|
|
10
|
+
"url": "https://fixture.example/articles/valid-publication",
|
|
11
|
+
"headers": {
|
|
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; includeSubDomains; preload",
|
|
14
|
+
"x-frame-options": "DENY",
|
|
15
|
+
"x-content-type-options": "nosniff",
|
|
16
|
+
"referrer-policy": "strict-origin-when-cross-origin",
|
|
17
|
+
"permissions-policy": "accelerometer=(), camera=(), geolocation=(), gyroscope=(), microphone=(), payment=(), usb=()",
|
|
18
|
+
"cache-control": "public, max-age=0, s-maxage=300, stale-while-revalidate=86400"
|
|
19
|
+
},
|
|
20
|
+
"html": "<!doctype html><html><head><title>Fixture publication conformance article</title><link rel=\"canonical\" href=\"https://fixture.example/articles/valid-publication\"><meta name=\"description\" content=\"A deterministic public article fixture for the Growth Labs conformance harness.\"><meta name=\"robots\" content=\"index, follow, max-image-preview:large\"><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},{\"url\":\"https://media.fixture.example/images/articles/valid-4x3.jpg\",\"width\":1200,\"height\":900},{\"url\":\"https://media.fixture.example/images/articles/valid-1x1.jpg\",\"width\":1200,\"height\":1200}]}</script></head><body><main><h1>Fixture publication conformance article</h1><p>Fixture body.</p></main></body></html>"
|
|
21
|
+
},
|
|
22
|
+
"expected": {
|
|
23
|
+
"canonicalUrl": "https://fixture.example/articles/valid-publication",
|
|
24
|
+
"schemaTypes": ["NewsArticle"],
|
|
25
|
+
"discoverEligible": true
|
|
26
|
+
},
|
|
27
|
+
"publicationSurfaces": [
|
|
28
|
+
{
|
|
29
|
+
"name": "sitemap",
|
|
30
|
+
"status": "present",
|
|
31
|
+
"url": "https://fixture.example/sitemap.xml",
|
|
32
|
+
"httpStatus": 200,
|
|
33
|
+
"contentType": "application/xml",
|
|
34
|
+
"bodyBytes": 640
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "robots",
|
|
38
|
+
"status": "present",
|
|
39
|
+
"url": "https://fixture.example/robots.txt",
|
|
40
|
+
"httpStatus": 200,
|
|
41
|
+
"contentType": "text/plain",
|
|
42
|
+
"bodyBytes": 180
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "feed",
|
|
46
|
+
"status": "present",
|
|
47
|
+
"url": "https://fixture.example/feed.xml",
|
|
48
|
+
"httpStatus": 200,
|
|
49
|
+
"contentType": "application/rss+xml",
|
|
50
|
+
"bodyBytes": 940
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"cacheVariants": [
|
|
54
|
+
{
|
|
55
|
+
"audience": "public",
|
|
56
|
+
"cacheKey": "fixture-news:public:/articles/valid-publication",
|
|
57
|
+
"url": "https://fixture.example/articles/valid-publication"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"audience": "crawler",
|
|
61
|
+
"cacheKey": "fixture-news:crawler:/articles/valid-publication",
|
|
62
|
+
"url": "https://fixture.example/articles/valid-publication"
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"buildEvidence": {
|
|
66
|
+
"commitSha": "bd39d718f6a5b0df9a249e11638a8b430050941e",
|
|
67
|
+
"artifactId": "fixture-build-001",
|
|
68
|
+
"generatedAt": "2026-07-15T23:30:00Z",
|
|
69
|
+
"packageVersions": {
|
|
70
|
+
"@growth-labs/conformance": "0.1.0",
|
|
71
|
+
"@growth-labs/seo": "0.9.0"
|
|
72
|
+
},
|
|
73
|
+
"staticHeaderPolicy": true,
|
|
74
|
+
"ssrHeaderPolicy": true
|
|
75
|
+
}
|
|
76
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@growth-labs/conformance",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Reusable fleet publication conformance harness for Growth Labs sites: headers, cache, metadata, structured data, images, publication surfaces, and evidence bundles.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./policy": {
|
|
13
|
+
"types": "./dist/policy.d.ts",
|
|
14
|
+
"import": "./dist/policy.js"
|
|
15
|
+
},
|
|
16
|
+
"./schema": {
|
|
17
|
+
"types": "./dist/schema.d.ts",
|
|
18
|
+
"import": "./dist/schema.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"src/**/*.ts",
|
|
24
|
+
"fixtures/**/*.json",
|
|
25
|
+
"README.md",
|
|
26
|
+
"CHANGELOG.md"
|
|
27
|
+
],
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public",
|
|
30
|
+
"registry": "https://registry.npmjs.org/"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"zod": "^3.25.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@cloudflare/workers-types": "^4.20260402.1",
|
|
37
|
+
"typescript": "^5.7.0",
|
|
38
|
+
"vitest": "^4.1.8"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc",
|
|
42
|
+
"dev": "tsc --watch",
|
|
43
|
+
"check": "biome check src/ __tests__ package.json README.md CHANGELOG.md",
|
|
44
|
+
"test": "tsc --noEmit -p tsconfig.test.json && vitest run",
|
|
45
|
+
"test:fixtures": "vitest run __tests__/negative-fixtures.test.ts",
|
|
46
|
+
"test:watch": "vitest"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/html.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
export interface JsonLdParseError {
|
|
2
|
+
message: string
|
|
3
|
+
path: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface JsonLdNode {
|
|
7
|
+
[key: string]: unknown
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ExtractedImage {
|
|
11
|
+
url?: string
|
|
12
|
+
width?: number
|
|
13
|
+
height?: number
|
|
14
|
+
path: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface HtmlSignals {
|
|
18
|
+
canonicalUrl?: string
|
|
19
|
+
robotsContent?: string
|
|
20
|
+
jsonLdNodes: JsonLdNode[]
|
|
21
|
+
jsonLdParseErrors: JsonLdParseError[]
|
|
22
|
+
jsonLdImages: ExtractedImage[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function extractHtmlSignals(html = ''): HtmlSignals {
|
|
26
|
+
const jsonLd = extractJsonLdNodes(html)
|
|
27
|
+
const jsonLdImages = jsonLd.nodes.flatMap((node, index) =>
|
|
28
|
+
extractImagesFromJsonLdValue(node.image, `jsonLd[${index}].image`),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
canonicalUrl: extractCanonicalUrl(html),
|
|
33
|
+
robotsContent: extractMetaContent(html, 'robots'),
|
|
34
|
+
jsonLdNodes: jsonLd.nodes,
|
|
35
|
+
jsonLdParseErrors: jsonLd.errors,
|
|
36
|
+
jsonLdImages,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getSchemaTypes(node: JsonLdNode): string[] {
|
|
41
|
+
const value = node['@type']
|
|
42
|
+
if (typeof value === 'string') return [value]
|
|
43
|
+
if (Array.isArray(value)) return value.filter((item): item is string => typeof item === 'string')
|
|
44
|
+
return []
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function nodeHasSchemaType(node: JsonLdNode, expectedType: string): boolean {
|
|
48
|
+
return getSchemaTypes(node).includes(expectedType)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function extractCanonicalUrl(html: string): string | undefined {
|
|
52
|
+
for (const tag of findOpeningTags(html, 'link')) {
|
|
53
|
+
const attrs = parseAttributes(tag)
|
|
54
|
+
if (attrs.rel?.split(/\s+/).some((value) => value.toLowerCase() === 'canonical')) {
|
|
55
|
+
return attrs.href
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function extractMetaContent(html: string, name: string): string | undefined {
|
|
62
|
+
const wanted = name.toLowerCase()
|
|
63
|
+
for (const tag of findOpeningTags(html, 'meta')) {
|
|
64
|
+
const attrs = parseAttributes(tag)
|
|
65
|
+
const metaName = attrs.name?.toLowerCase() ?? attrs.property?.toLowerCase()
|
|
66
|
+
if (metaName === wanted) return attrs.content
|
|
67
|
+
}
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function extractJsonLdNodes(html: string): { nodes: JsonLdNode[]; errors: JsonLdParseError[] } {
|
|
72
|
+
const nodes: JsonLdNode[] = []
|
|
73
|
+
const errors: JsonLdParseError[] = []
|
|
74
|
+
const scriptPattern = /<script\b[^>]*>([\s\S]*?)<\/script>/gi
|
|
75
|
+
let scriptIndex = 0
|
|
76
|
+
|
|
77
|
+
for (const match of html.matchAll(scriptPattern)) {
|
|
78
|
+
const fullTag = match[0] ?? ''
|
|
79
|
+
const openTag = fullTag.slice(0, fullTag.indexOf('>') + 1)
|
|
80
|
+
const attrs = parseAttributes(openTag)
|
|
81
|
+
if ((attrs.type ?? '').toLowerCase() !== 'application/ld+json') continue
|
|
82
|
+
|
|
83
|
+
const body = match[1] ?? ''
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(body.trim()) as unknown
|
|
86
|
+
collectJsonLdNodes(parsed, nodes)
|
|
87
|
+
} catch (error) {
|
|
88
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
89
|
+
errors.push({ message, path: `script[application/ld+json][${scriptIndex}]` })
|
|
90
|
+
}
|
|
91
|
+
scriptIndex += 1
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { nodes, errors }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function collectJsonLdNodes(value: unknown, out: JsonLdNode[]): void {
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
for (const item of value) collectJsonLdNodes(item, out)
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!isRecord(value)) return
|
|
104
|
+
out.push(value)
|
|
105
|
+
|
|
106
|
+
const graph = value['@graph']
|
|
107
|
+
if (Array.isArray(graph)) {
|
|
108
|
+
for (const item of graph) collectJsonLdNodes(item, out)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function extractImagesFromJsonLdValue(value: unknown, path: string): ExtractedImage[] {
|
|
113
|
+
if (Array.isArray(value)) {
|
|
114
|
+
return value.flatMap((item, index) => extractImagesFromJsonLdValue(item, `${path}[${index}]`))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (typeof value === 'string') {
|
|
118
|
+
return [{ url: value, path }]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!isRecord(value)) return []
|
|
122
|
+
|
|
123
|
+
const url = firstString(value.url, value.contentUrl, value['@id'])
|
|
124
|
+
const width = toFiniteNumber(value.width)
|
|
125
|
+
const height = toFiniteNumber(value.height)
|
|
126
|
+
|
|
127
|
+
return [{ url, width, height, path }]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function findOpeningTags(html: string, tagName: string): string[] {
|
|
131
|
+
const pattern = new RegExp(`<${tagName}\\b[^>]*>`, 'gi')
|
|
132
|
+
return [...html.matchAll(pattern)].map((match) => match[0] ?? '')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseAttributes(tag: string): Record<string, string> {
|
|
136
|
+
const attrs: Record<string, string> = {}
|
|
137
|
+
const attributePattern = /([A-Za-z_:][-A-Za-z0-9_:.]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/g
|
|
138
|
+
|
|
139
|
+
for (const match of tag.matchAll(attributePattern)) {
|
|
140
|
+
const name = match[1]?.toLowerCase()
|
|
141
|
+
if (!name) continue
|
|
142
|
+
attrs[name] = decodeHtmlEntities(match[2] ?? match[3] ?? match[4] ?? '')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return attrs
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function firstString(...values: unknown[]): string | undefined {
|
|
149
|
+
for (const value of values) {
|
|
150
|
+
if (typeof value === 'string' && value.trim() !== '') return value
|
|
151
|
+
}
|
|
152
|
+
return undefined
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function toFiniteNumber(value: unknown): number | undefined {
|
|
156
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
157
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
158
|
+
const parsed = Number(value)
|
|
159
|
+
if (Number.isFinite(parsed)) return parsed
|
|
160
|
+
}
|
|
161
|
+
return undefined
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function decodeHtmlEntities(value: string): string {
|
|
165
|
+
return value
|
|
166
|
+
.replaceAll('&', '&')
|
|
167
|
+
.replaceAll('"', '"')
|
|
168
|
+
.replaceAll(''', "'")
|
|
169
|
+
.replaceAll('<', '<')
|
|
170
|
+
.replaceAll('>', '>')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isRecord(value: unknown): value is JsonLdNode {
|
|
174
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
175
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export {
|
|
2
|
+
buildCacheControl,
|
|
3
|
+
buildContentSecurityPolicy,
|
|
4
|
+
buildSharedPublicationHeaders,
|
|
5
|
+
type CachePolicyOptions,
|
|
6
|
+
type HeaderPolicyOptions,
|
|
7
|
+
isPrivateAudience,
|
|
8
|
+
normalizeHeaders,
|
|
9
|
+
REQUIRED_SECURITY_HEADERS,
|
|
10
|
+
} from './policy.js'
|
|
11
|
+
export { type RunConformanceOptions, runFleetConformance, runSiteConformance } from './runner.js'
|
|
12
|
+
export {
|
|
13
|
+
type Audience,
|
|
14
|
+
audienceSchema,
|
|
15
|
+
type BuildEvidence,
|
|
16
|
+
buildEvidenceSchema,
|
|
17
|
+
type CacheVariant,
|
|
18
|
+
type ConformanceCheckReceipt,
|
|
19
|
+
type ConformanceFinding,
|
|
20
|
+
type ConformanceFixture,
|
|
21
|
+
type ConformanceFixtureInput,
|
|
22
|
+
cacheVariantSchema,
|
|
23
|
+
checkStatusSchema,
|
|
24
|
+
conformanceCheckReceiptSchema,
|
|
25
|
+
conformanceExpectedSchema,
|
|
26
|
+
conformanceFindingSchema,
|
|
27
|
+
conformanceFixtureSchema,
|
|
28
|
+
EVIDENCE_SCHEMA_VERSION,
|
|
29
|
+
type EvidenceSummary,
|
|
30
|
+
evidenceSummarySchema,
|
|
31
|
+
type FleetEvidenceBundle,
|
|
32
|
+
findingSeveritySchema,
|
|
33
|
+
fleetEvidenceBundleSchema,
|
|
34
|
+
type HeaderMap,
|
|
35
|
+
headerMapSchema,
|
|
36
|
+
headerValueSchema,
|
|
37
|
+
type PublicationSurfaceEvidence,
|
|
38
|
+
parseConformanceFixture,
|
|
39
|
+
publicationSurfaceSchema,
|
|
40
|
+
REQUIRED_NEGATIVE_FIXTURE_CODES,
|
|
41
|
+
type RenderMode,
|
|
42
|
+
type RequiredNegativeFindingCode,
|
|
43
|
+
type RequiredNegativeFixtureId,
|
|
44
|
+
renderModeSchema,
|
|
45
|
+
type SiteEvidenceBundle,
|
|
46
|
+
type SurfaceResponse,
|
|
47
|
+
siteEvidenceBundleSchema,
|
|
48
|
+
surfaceResponseSchema,
|
|
49
|
+
validateFleetEvidenceBundle,
|
|
50
|
+
validateSiteEvidenceBundle,
|
|
51
|
+
} from './schema.js'
|