afdocs 0.2.0 → 0.3.1
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/README.md +13 -7
- package/dist/checks/authentication/auth-gate-detection.js +163 -5
- package/dist/checks/authentication/auth-gate-detection.js.map +1 -1
- package/dist/checks/content-structure/markdown-code-fence-validity.js +119 -5
- package/dist/checks/content-structure/markdown-code-fence-validity.js.map +1 -1
- package/dist/checks/observability/cache-header-hygiene.js +200 -5
- package/dist/checks/observability/cache-header-hygiene.js.map +1 -1
- package/dist/checks/page-size/page-size-markdown.js +43 -126
- package/dist/checks/page-size/page-size-markdown.js.map +1 -1
- package/dist/checks/url-stability/http-status-codes.js +106 -5
- package/dist/checks/url-stability/http-status-codes.js.map +1 -1
- package/dist/cli/formatters/text.js +2 -2
- package/dist/cli/formatters/text.js.map +1 -1
- package/dist/helpers/get-markdown-content.d.ts +23 -0
- package/dist/helpers/get-markdown-content.d.ts.map +1 -0
- package/dist/helpers/get-markdown-content.js +93 -0
- package/dist/helpers/get-markdown-content.js.map +1 -0
- package/dist/helpers/get-page-urls.d.ts +3 -0
- package/dist/helpers/get-page-urls.d.ts.map +1 -1
- package/dist/helpers/get-page-urls.js +7 -1
- package/dist/helpers/get-page-urls.js.map +1 -1
- package/dist/helpers/to-md-urls.d.ts.map +1 -1
- package/dist/helpers/to-md-urls.js +7 -2
- package/dist/helpers/to-md-urls.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Test your documentation site against the [Agent-Friendly Documentation Spec](htt
|
|
|
7
7
|
|
|
8
8
|
Agents don't use docs like humans. They hit truncation limits, get walls of CSS instead of content, can't follow cross-host redirects, and don't know about quality-of-life improvements like `llms.txt` or `.md` docs pages that would make life swell. Maybe this is because the industry has lacked guidance - until now.
|
|
9
9
|
|
|
10
|
-
afdocs runs 21 checks across 8 categories to evaluate how well your docs serve agent consumers.
|
|
10
|
+
afdocs runs 21 checks across 8 categories to evaluate how well your docs serve agent consumers. 14 are fully implemented; the rest return `skip` until completed.
|
|
11
11
|
|
|
12
12
|
> **Status: Early development (0.x)**
|
|
13
13
|
> This project is under active development. Check IDs, CLI flags, and output formats may change between minor versions. Feel free to try it out, but don't build automation against specific output until 1.0.
|
|
@@ -36,8 +36,14 @@ Markdown Availability
|
|
|
36
36
|
✗ content-negotiation: Server ignores Accept: text/markdown header (0/50 sampled pages return markdown)
|
|
37
37
|
✗ markdown-url-support: No sampled pages support .md URLs (0/50 tested)
|
|
38
38
|
|
|
39
|
+
URL Stability
|
|
40
|
+
✓ http-status-codes: All 50 sampled pages return proper error codes for bad URLs
|
|
41
|
+
|
|
42
|
+
Authentication
|
|
43
|
+
✓ auth-gate-detection: All 50 sampled pages are publicly accessible
|
|
44
|
+
|
|
39
45
|
Summary
|
|
40
|
-
|
|
46
|
+
9 passed, 3 failed, 9 skipped (21 total)
|
|
41
47
|
```
|
|
42
48
|
|
|
43
49
|
## Install
|
|
@@ -138,7 +144,7 @@ describe('agent-friendliness', () => {
|
|
|
138
144
|
|
|
139
145
|
## Checks
|
|
140
146
|
|
|
141
|
-
21 checks across 8 categories. Checks marked with \* are
|
|
147
|
+
21 checks across 8 categories. Checks marked with \* are not yet implemented and return `skip`.
|
|
142
148
|
|
|
143
149
|
### Category 1: llms.txt
|
|
144
150
|
|
|
@@ -171,13 +177,13 @@ describe('agent-friendliness', () => {
|
|
|
171
177
|
| --------------------------------- | -------------------------------------------------- |
|
|
172
178
|
| `tabbed-content-serialization` \* | Whether tabbed content creates oversized output |
|
|
173
179
|
| `section-header-quality` \* | Whether headers in tabbed sections include context |
|
|
174
|
-
| `markdown-code-fence-validity`
|
|
180
|
+
| `markdown-code-fence-validity` | Whether markdown has unclosed code fences |
|
|
175
181
|
|
|
176
182
|
### Category 5: URL Stability and Redirects
|
|
177
183
|
|
|
178
184
|
| Check | Description |
|
|
179
185
|
| ---------------------- | ----------------------------------------------- |
|
|
180
|
-
| `http-status-codes`
|
|
186
|
+
| `http-status-codes` | Whether error pages return correct status codes |
|
|
181
187
|
| `redirect-behavior` \* | Whether redirects are same-host HTTP redirects |
|
|
182
188
|
|
|
183
189
|
### Category 6: Agent Discoverability Directives
|
|
@@ -192,13 +198,13 @@ describe('agent-friendliness', () => {
|
|
|
192
198
|
| ---------------------------- | ---------------------------------------------- |
|
|
193
199
|
| `llms-txt-freshness` \* | Whether `llms.txt` reflects current site state |
|
|
194
200
|
| `markdown-content-parity` \* | Whether markdown and HTML versions match |
|
|
195
|
-
| `cache-header-hygiene`
|
|
201
|
+
| `cache-header-hygiene` | Whether cache headers allow timely updates |
|
|
196
202
|
|
|
197
203
|
### Category 8: Authentication and Access
|
|
198
204
|
|
|
199
205
|
| Check | Description |
|
|
200
206
|
| ---------------------------- | -------------------------------------------------------------------- |
|
|
201
|
-
| `auth-gate-detection`
|
|
207
|
+
| `auth-gate-detection` | Whether documentation pages require authentication to access content |
|
|
202
208
|
| `auth-alternative-access` \* | Whether auth-gated sites provide alternative access paths for agents |
|
|
203
209
|
|
|
204
210
|
## Check dependencies
|
|
@@ -1,10 +1,168 @@
|
|
|
1
1
|
import { registerCheck } from '../registry.js';
|
|
2
|
-
|
|
2
|
+
import { discoverAndSamplePages } from '../../helpers/get-page-urls.js';
|
|
3
|
+
const SSO_DOMAINS = [
|
|
4
|
+
'okta.com',
|
|
5
|
+
'auth0.com',
|
|
6
|
+
'login.microsoftonline.com',
|
|
7
|
+
'accounts.google.com',
|
|
8
|
+
'login.salesforce.com',
|
|
9
|
+
'sso.',
|
|
10
|
+
'idp.',
|
|
11
|
+
'auth.',
|
|
12
|
+
'login.',
|
|
13
|
+
];
|
|
14
|
+
function isSsoDomain(url) {
|
|
15
|
+
try {
|
|
16
|
+
const hostname = new URL(url).hostname.toLowerCase();
|
|
17
|
+
return SSO_DOMAINS.find((domain) => hostname === domain || hostname.endsWith('.' + domain) || hostname.startsWith(domain));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function detectLoginForm(body) {
|
|
24
|
+
const sample = body.slice(0, 50000).toLowerCase();
|
|
25
|
+
if (sample.includes('<input') && sample.includes('type="password"')) {
|
|
26
|
+
return 'Contains password input field';
|
|
27
|
+
}
|
|
28
|
+
// Check page title for login indicators
|
|
29
|
+
const titleMatch = /<title[^>]*>(.*?)<\/title>/i.exec(sample);
|
|
30
|
+
if (titleMatch) {
|
|
31
|
+
const title = titleMatch[1].toLowerCase();
|
|
32
|
+
if (/sign\s*in|log\s*in|authenticate/i.test(title)) {
|
|
33
|
+
return `Page title suggests login: "${titleMatch[1].trim()}"`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Check for SSO form actions
|
|
37
|
+
if (/<form[^>]*action\s*=\s*["'][^"']*(?:saml|oauth|openid|sso|auth)[^"']*["']/i.test(sample)) {
|
|
38
|
+
return 'Contains SSO-related form action';
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
async function check(ctx) {
|
|
43
|
+
const id = 'auth-gate-detection';
|
|
44
|
+
const category = 'authentication';
|
|
45
|
+
const { urls: pageUrls, totalPages, sampled, warnings } = await discoverAndSamplePages(ctx);
|
|
46
|
+
const results = [];
|
|
47
|
+
const concurrency = ctx.options.maxConcurrency;
|
|
48
|
+
for (let i = 0; i < pageUrls.length; i += concurrency) {
|
|
49
|
+
const batch = pageUrls.slice(i, i + concurrency);
|
|
50
|
+
const batchResults = await Promise.all(batch.map(async (url) => {
|
|
51
|
+
try {
|
|
52
|
+
const response = await ctx.http.fetch(url, { redirect: 'manual' });
|
|
53
|
+
const status = response.status;
|
|
54
|
+
// Auth-required status codes
|
|
55
|
+
if (status === 401 || status === 403) {
|
|
56
|
+
return { url, classification: 'auth-required', status };
|
|
57
|
+
}
|
|
58
|
+
// Redirect — check if it's to an SSO domain
|
|
59
|
+
if (status >= 300 && status < 400) {
|
|
60
|
+
const location = response.headers.get('location');
|
|
61
|
+
if (location) {
|
|
62
|
+
const resolvedLocation = location.startsWith('http')
|
|
63
|
+
? location
|
|
64
|
+
: new URL(location, url).toString();
|
|
65
|
+
const ssoDomain = isSsoDomain(resolvedLocation);
|
|
66
|
+
if (ssoDomain) {
|
|
67
|
+
return {
|
|
68
|
+
url,
|
|
69
|
+
classification: 'auth-redirect',
|
|
70
|
+
status,
|
|
71
|
+
redirectUrl: resolvedLocation,
|
|
72
|
+
ssoDomain,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Non-SSO redirect — treat as accessible (normal redirect)
|
|
77
|
+
return { url, classification: 'accessible', status };
|
|
78
|
+
}
|
|
79
|
+
// 200 — check for soft auth gate (login form)
|
|
80
|
+
if (status === 200) {
|
|
81
|
+
let body;
|
|
82
|
+
try {
|
|
83
|
+
body = await response.text();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return { url, classification: 'accessible', status };
|
|
87
|
+
}
|
|
88
|
+
const loginHint = detectLoginForm(body);
|
|
89
|
+
if (loginHint) {
|
|
90
|
+
return { url, classification: 'soft-auth-gate', status, hint: loginHint };
|
|
91
|
+
}
|
|
92
|
+
return { url, classification: 'accessible', status };
|
|
93
|
+
}
|
|
94
|
+
// Other status codes — treat as accessible
|
|
95
|
+
return { url, classification: 'accessible', status };
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
return {
|
|
99
|
+
url,
|
|
100
|
+
classification: 'accessible',
|
|
101
|
+
status: null,
|
|
102
|
+
error: err instanceof Error ? err.message : String(err),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}));
|
|
106
|
+
results.push(...batchResults);
|
|
107
|
+
}
|
|
108
|
+
const fetchErrors = results.filter((r) => r.error).length;
|
|
109
|
+
const tested = results.filter((r) => !r.error);
|
|
110
|
+
if (tested.length === 0) {
|
|
111
|
+
return {
|
|
112
|
+
id,
|
|
113
|
+
category,
|
|
114
|
+
status: 'fail',
|
|
115
|
+
message: `Could not fetch any pages to check authentication${fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : ''}`,
|
|
116
|
+
details: {
|
|
117
|
+
totalPages,
|
|
118
|
+
testedPages: results.length,
|
|
119
|
+
sampled,
|
|
120
|
+
fetchErrors,
|
|
121
|
+
pageResults: results,
|
|
122
|
+
discoveryWarnings: warnings,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const accessible = tested.filter((r) => r.classification === 'accessible');
|
|
127
|
+
const authRequired = tested.filter((r) => r.classification === 'auth-required');
|
|
128
|
+
const softAuthGate = tested.filter((r) => r.classification === 'soft-auth-gate');
|
|
129
|
+
const authRedirect = tested.filter((r) => r.classification === 'auth-redirect');
|
|
130
|
+
const gatedCount = authRequired.length + softAuthGate.length + authRedirect.length;
|
|
131
|
+
const ssoDomains = [...new Set(authRedirect.map((r) => r.ssoDomain).filter(Boolean))];
|
|
132
|
+
let status;
|
|
133
|
+
let message;
|
|
134
|
+
const suffix = fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : '';
|
|
135
|
+
const pageLabel = sampled ? 'sampled pages' : 'pages';
|
|
136
|
+
if (gatedCount === 0) {
|
|
137
|
+
status = 'pass';
|
|
138
|
+
message = `All ${accessible.length} ${pageLabel} are publicly accessible${suffix}`;
|
|
139
|
+
}
|
|
140
|
+
else if (accessible.length > 0 && gatedCount > 0) {
|
|
141
|
+
status = 'warn';
|
|
142
|
+
message = `${gatedCount} of ${tested.length} ${pageLabel} require authentication (${accessible.length} accessible)${suffix}`;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
status = 'fail';
|
|
146
|
+
message = `All ${tested.length} ${pageLabel} require authentication${suffix}`;
|
|
147
|
+
}
|
|
3
148
|
return {
|
|
4
|
-
id
|
|
5
|
-
category
|
|
6
|
-
status
|
|
7
|
-
message
|
|
149
|
+
id,
|
|
150
|
+
category,
|
|
151
|
+
status,
|
|
152
|
+
message,
|
|
153
|
+
details: {
|
|
154
|
+
totalPages,
|
|
155
|
+
testedPages: results.length,
|
|
156
|
+
sampled,
|
|
157
|
+
accessible: accessible.length,
|
|
158
|
+
authRequired: authRequired.length,
|
|
159
|
+
softAuthGate: softAuthGate.length,
|
|
160
|
+
authRedirect: authRedirect.length,
|
|
161
|
+
ssoDomains,
|
|
162
|
+
fetchErrors,
|
|
163
|
+
pageResults: results,
|
|
164
|
+
discoveryWarnings: warnings,
|
|
165
|
+
},
|
|
8
166
|
};
|
|
9
167
|
}
|
|
10
168
|
registerCheck({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-gate-detection.js","sourceRoot":"","sources":["../../../src/checks/authentication/auth-gate-detection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"auth-gate-detection.js","sourceRoot":"","sources":["../../../src/checks/authentication/auth-gate-detection.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,10 +1,124 @@
|
|
|
1
1
|
import { registerCheck } from '../registry.js';
|
|
2
|
-
|
|
2
|
+
import { getMarkdownContent } from '../../helpers/get-markdown-content.js';
|
|
3
|
+
/**
|
|
4
|
+
* Strip blockquote prefixes (`> `) from a line so that fences inside
|
|
5
|
+
* blockquotes are detected the same way a CommonMark parser would
|
|
6
|
+
* handle them. Supports nested blockquotes (`> > `).
|
|
7
|
+
*/
|
|
8
|
+
function stripBlockquotePrefix(line) {
|
|
9
|
+
return line.replace(/^(?:\s{0,3}> ?)+/, '');
|
|
10
|
+
}
|
|
11
|
+
/** Match a line that opens or closes a code fence (after blockquote stripping). */
|
|
12
|
+
const FENCE_RE = /^( {0,3})((`{3,})|(~{3,}))(.*)?$/;
|
|
13
|
+
function analyzeFences(content) {
|
|
14
|
+
const lines = content.split('\n');
|
|
15
|
+
const issues = [];
|
|
16
|
+
let fenceCount = 0;
|
|
17
|
+
let openFence = null;
|
|
18
|
+
for (let i = 0; i < lines.length; i++) {
|
|
19
|
+
const stripped = stripBlockquotePrefix(lines[i]);
|
|
20
|
+
const match = FENCE_RE.exec(stripped);
|
|
21
|
+
if (!match)
|
|
22
|
+
continue;
|
|
23
|
+
const char = match[3] ? '`' : '~';
|
|
24
|
+
const length = (match[3] || match[4]).length;
|
|
25
|
+
if (!openFence) {
|
|
26
|
+
// Opening fence
|
|
27
|
+
openFence = { line: i + 1, char, length };
|
|
28
|
+
fenceCount++;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
// Potential closing fence: must use same char and be at least as long
|
|
32
|
+
if (char === openFence.char && length >= openFence.length) {
|
|
33
|
+
// Proper close
|
|
34
|
+
openFence = null;
|
|
35
|
+
}
|
|
36
|
+
else if (char !== openFence.char && length >= openFence.length) {
|
|
37
|
+
// Inconsistent close: different delimiter type
|
|
38
|
+
issues.push({
|
|
39
|
+
line: i + 1,
|
|
40
|
+
type: 'inconsistent-close',
|
|
41
|
+
opener: openFence.char.repeat(openFence.length),
|
|
42
|
+
closer: char.repeat(length),
|
|
43
|
+
});
|
|
44
|
+
openFence = null;
|
|
45
|
+
}
|
|
46
|
+
// Otherwise: different char + shorter length = not a close, just content inside the fence
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (openFence) {
|
|
50
|
+
issues.push({
|
|
51
|
+
line: openFence.line,
|
|
52
|
+
type: 'unclosed',
|
|
53
|
+
opener: openFence.char.repeat(openFence.length),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return { fenceCount, issues };
|
|
57
|
+
}
|
|
58
|
+
function worstStatus(statuses) {
|
|
59
|
+
if (statuses.includes('fail'))
|
|
60
|
+
return 'fail';
|
|
61
|
+
if (statuses.includes('warn'))
|
|
62
|
+
return 'warn';
|
|
63
|
+
return 'pass';
|
|
64
|
+
}
|
|
65
|
+
async function check(ctx) {
|
|
66
|
+
const id = 'markdown-code-fence-validity';
|
|
67
|
+
const category = 'content-structure';
|
|
68
|
+
const mdResult = await getMarkdownContent(ctx);
|
|
69
|
+
if (mdResult.pages.length === 0) {
|
|
70
|
+
if (mdResult.mode === 'cached' && !mdResult.depPassed) {
|
|
71
|
+
return {
|
|
72
|
+
id,
|
|
73
|
+
category,
|
|
74
|
+
status: 'skip',
|
|
75
|
+
message: 'Site does not serve markdown content; nothing to analyze',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const hint = mdResult.mode === 'standalone'
|
|
79
|
+
? '; try running with markdown-url-support or content-negotiation checks'
|
|
80
|
+
: '';
|
|
81
|
+
return { id, category, status: 'skip', message: `No markdown content found${hint}` };
|
|
82
|
+
}
|
|
83
|
+
const results = mdResult.pages.map(({ url, content, source }) => {
|
|
84
|
+
const { fenceCount, issues } = analyzeFences(content);
|
|
85
|
+
const hasUnclosed = issues.some((i) => i.type === 'unclosed');
|
|
86
|
+
const hasInconsistent = issues.some((i) => i.type === 'inconsistent-close');
|
|
87
|
+
let status;
|
|
88
|
+
if (hasUnclosed)
|
|
89
|
+
status = 'fail';
|
|
90
|
+
else if (hasInconsistent)
|
|
91
|
+
status = 'warn';
|
|
92
|
+
else
|
|
93
|
+
status = 'pass';
|
|
94
|
+
return { url, source, fenceCount, issues, status };
|
|
95
|
+
});
|
|
96
|
+
const overallStatus = worstStatus(results.map((r) => r.status));
|
|
97
|
+
const totalFences = results.reduce((sum, r) => sum + r.fenceCount, 0);
|
|
98
|
+
const unclosedCount = results.reduce((sum, r) => sum + r.issues.filter((i) => i.type === 'unclosed').length, 0);
|
|
99
|
+
const inconsistentCount = results.reduce((sum, r) => sum + r.issues.filter((i) => i.type === 'inconsistent-close').length, 0);
|
|
100
|
+
let message;
|
|
101
|
+
if (overallStatus === 'pass') {
|
|
102
|
+
message = `All ${totalFences} code fences properly closed across ${results.length} pages`;
|
|
103
|
+
}
|
|
104
|
+
else if (overallStatus === 'warn') {
|
|
105
|
+
message = `${inconsistentCount} code fences use inconsistent delimiters across ${results.length} pages`;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
message = `${unclosedCount} unclosed code fences found across ${results.length} pages`;
|
|
109
|
+
}
|
|
3
110
|
return {
|
|
4
|
-
id
|
|
5
|
-
category
|
|
6
|
-
status:
|
|
7
|
-
message
|
|
111
|
+
id,
|
|
112
|
+
category,
|
|
113
|
+
status: overallStatus,
|
|
114
|
+
message,
|
|
115
|
+
details: {
|
|
116
|
+
pagesAnalyzed: results.length,
|
|
117
|
+
totalFences,
|
|
118
|
+
unclosedCount,
|
|
119
|
+
inconsistentCount,
|
|
120
|
+
pageResults: results,
|
|
121
|
+
},
|
|
8
122
|
};
|
|
9
123
|
}
|
|
10
124
|
registerCheck({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown-code-fence-validity.js","sourceRoot":"","sources":["../../../src/checks/content-structure/markdown-code-fence-validity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"markdown-code-fence-validity.js","sourceRoot":"","sources":["../../../src/checks/content-structure/markdown-code-fence-validity.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,10 +1,205 @@
|
|
|
1
1
|
import { registerCheck } from '../registry.js';
|
|
2
|
-
|
|
2
|
+
import { discoverAndSamplePages } from '../../helpers/get-page-urls.js';
|
|
3
|
+
function parseCacheControl(header) {
|
|
4
|
+
if (!header)
|
|
5
|
+
return { maxAge: null, sMaxAge: null, mustRevalidate: false, noCache: false, noStore: false };
|
|
6
|
+
const directives = header
|
|
7
|
+
.toLowerCase()
|
|
8
|
+
.split(',')
|
|
9
|
+
.map((d) => d.trim());
|
|
10
|
+
let maxAge = null;
|
|
11
|
+
let sMaxAge = null;
|
|
12
|
+
let mustRevalidate = false;
|
|
13
|
+
let noCache = false;
|
|
14
|
+
let noStore = false;
|
|
15
|
+
for (const d of directives) {
|
|
16
|
+
if (d.startsWith('max-age=')) {
|
|
17
|
+
maxAge = parseInt(d.split('=')[1], 10);
|
|
18
|
+
if (isNaN(maxAge))
|
|
19
|
+
maxAge = null;
|
|
20
|
+
}
|
|
21
|
+
else if (d.startsWith('s-maxage=')) {
|
|
22
|
+
sMaxAge = parseInt(d.split('=')[1], 10);
|
|
23
|
+
if (isNaN(sMaxAge))
|
|
24
|
+
sMaxAge = null;
|
|
25
|
+
}
|
|
26
|
+
else if (d === 'must-revalidate') {
|
|
27
|
+
mustRevalidate = true;
|
|
28
|
+
}
|
|
29
|
+
else if (d === 'no-cache') {
|
|
30
|
+
noCache = true;
|
|
31
|
+
}
|
|
32
|
+
else if (d === 'no-store') {
|
|
33
|
+
noStore = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { maxAge, sMaxAge, mustRevalidate, noCache, noStore };
|
|
37
|
+
}
|
|
38
|
+
function classifyCache(result) {
|
|
39
|
+
// no-cache or no-store = always fresh
|
|
40
|
+
if (result.noCache || result.noStore)
|
|
41
|
+
return 'pass';
|
|
42
|
+
// must-revalidate with a revalidation mechanism is good
|
|
43
|
+
if (result.mustRevalidate && (result.etag || result.lastModified))
|
|
44
|
+
return 'pass';
|
|
45
|
+
const effective = result.effectiveMaxAge;
|
|
46
|
+
if (effective === null) {
|
|
47
|
+
// No cache-related headers at all
|
|
48
|
+
if (!result.cacheControl && !result.expires && !result.etag && !result.lastModified) {
|
|
49
|
+
return 'fail';
|
|
50
|
+
}
|
|
51
|
+
// Has ETag/Last-Modified but no max-age — still ok (browser will revalidate)
|
|
52
|
+
if (result.etag || result.lastModified)
|
|
53
|
+
return 'pass';
|
|
54
|
+
return 'fail';
|
|
55
|
+
}
|
|
56
|
+
if (effective <= 3600)
|
|
57
|
+
return 'pass';
|
|
58
|
+
if (effective <= 86400)
|
|
59
|
+
return 'warn';
|
|
60
|
+
return 'fail';
|
|
61
|
+
}
|
|
62
|
+
function getEffectiveMaxAge(parsed, expires) {
|
|
63
|
+
// s-maxage takes precedence over max-age
|
|
64
|
+
if (parsed.sMaxAge !== null)
|
|
65
|
+
return parsed.sMaxAge;
|
|
66
|
+
if (parsed.maxAge !== null)
|
|
67
|
+
return parsed.maxAge;
|
|
68
|
+
// Fall back to Expires header
|
|
69
|
+
if (expires) {
|
|
70
|
+
try {
|
|
71
|
+
const expiresMs = new Date(expires).getTime();
|
|
72
|
+
const nowMs = Date.now();
|
|
73
|
+
if (!isNaN(expiresMs)) {
|
|
74
|
+
return Math.max(0, Math.round((expiresMs - nowMs) / 1000));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// invalid date
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
function worstStatus(statuses) {
|
|
84
|
+
if (statuses.includes('fail'))
|
|
85
|
+
return 'fail';
|
|
86
|
+
if (statuses.includes('warn'))
|
|
87
|
+
return 'warn';
|
|
88
|
+
return 'pass';
|
|
89
|
+
}
|
|
90
|
+
async function check(ctx) {
|
|
91
|
+
const id = 'cache-header-hygiene';
|
|
92
|
+
const category = 'observability';
|
|
93
|
+
// Collect URLs to check: llms.txt files + sampled page URLs
|
|
94
|
+
const urlsToCheck = [];
|
|
95
|
+
// llms.txt URLs
|
|
96
|
+
const existsResult = ctx.previousResults.get('llms-txt-exists');
|
|
97
|
+
const discovered = (existsResult?.details?.discoveredFiles ?? []);
|
|
98
|
+
for (const file of discovered) {
|
|
99
|
+
urlsToCheck.push(file.url);
|
|
100
|
+
}
|
|
101
|
+
// Page URLs
|
|
102
|
+
const { urls: pageUrls, totalPages, sampled, warnings } = await discoverAndSamplePages(ctx);
|
|
103
|
+
for (const url of pageUrls) {
|
|
104
|
+
if (!urlsToCheck.includes(url)) {
|
|
105
|
+
urlsToCheck.push(url);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const results = [];
|
|
109
|
+
const concurrency = ctx.options.maxConcurrency;
|
|
110
|
+
for (let i = 0; i < urlsToCheck.length; i += concurrency) {
|
|
111
|
+
const batch = urlsToCheck.slice(i, i + concurrency);
|
|
112
|
+
const batchResults = await Promise.all(batch.map(async (url) => {
|
|
113
|
+
try {
|
|
114
|
+
const response = await ctx.http.fetch(url);
|
|
115
|
+
const ccHeader = response.headers.get('cache-control');
|
|
116
|
+
const parsed = parseCacheControl(ccHeader);
|
|
117
|
+
const etag = response.headers.get('etag');
|
|
118
|
+
const lastModified = response.headers.get('last-modified');
|
|
119
|
+
const expires = response.headers.get('expires');
|
|
120
|
+
const effectiveMaxAge = getEffectiveMaxAge(parsed, expires);
|
|
121
|
+
const partial = {
|
|
122
|
+
url,
|
|
123
|
+
cacheControl: ccHeader,
|
|
124
|
+
maxAge: parsed.maxAge,
|
|
125
|
+
sMaxAge: parsed.sMaxAge,
|
|
126
|
+
mustRevalidate: parsed.mustRevalidate,
|
|
127
|
+
noCache: parsed.noCache,
|
|
128
|
+
noStore: parsed.noStore,
|
|
129
|
+
etag,
|
|
130
|
+
lastModified,
|
|
131
|
+
expires,
|
|
132
|
+
effectiveMaxAge,
|
|
133
|
+
};
|
|
134
|
+
return { ...partial, status: classifyCache(partial) };
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
return {
|
|
138
|
+
url,
|
|
139
|
+
cacheControl: null,
|
|
140
|
+
maxAge: null,
|
|
141
|
+
sMaxAge: null,
|
|
142
|
+
mustRevalidate: false,
|
|
143
|
+
noCache: false,
|
|
144
|
+
noStore: false,
|
|
145
|
+
etag: null,
|
|
146
|
+
lastModified: null,
|
|
147
|
+
expires: null,
|
|
148
|
+
effectiveMaxAge: null,
|
|
149
|
+
status: 'fail',
|
|
150
|
+
error: err instanceof Error ? err.message : String(err),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}));
|
|
154
|
+
results.push(...batchResults);
|
|
155
|
+
}
|
|
156
|
+
const successful = results.filter((r) => !r.error);
|
|
157
|
+
const fetchErrors = results.filter((r) => r.error).length;
|
|
158
|
+
if (successful.length === 0) {
|
|
159
|
+
return {
|
|
160
|
+
id,
|
|
161
|
+
category,
|
|
162
|
+
status: 'fail',
|
|
163
|
+
message: `Could not fetch any endpoints to check cache headers${fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : ''}`,
|
|
164
|
+
details: {
|
|
165
|
+
testedEndpoints: results.length,
|
|
166
|
+
fetchErrors,
|
|
167
|
+
endpointResults: results,
|
|
168
|
+
discoveryWarnings: warnings,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const overallStatus = worstStatus(successful.map((r) => r.status));
|
|
173
|
+
const passBucket = successful.filter((r) => r.status === 'pass').length;
|
|
174
|
+
const warnBucket = successful.filter((r) => r.status === 'warn').length;
|
|
175
|
+
const failBucket = successful.filter((r) => r.status === 'fail').length;
|
|
176
|
+
const suffix = fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : '';
|
|
177
|
+
let message;
|
|
178
|
+
if (overallStatus === 'pass') {
|
|
179
|
+
message = `All ${successful.length} endpoints have appropriate cache headers${suffix}`;
|
|
180
|
+
}
|
|
181
|
+
else if (overallStatus === 'warn') {
|
|
182
|
+
message = `${warnBucket} of ${successful.length} endpoints have moderate cache lifetimes (1–24 hours)${suffix}`;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
message = `${failBucket} of ${successful.length} endpoints have aggressive caching or missing cache headers${suffix}`;
|
|
186
|
+
}
|
|
3
187
|
return {
|
|
4
|
-
id
|
|
5
|
-
category
|
|
6
|
-
status:
|
|
7
|
-
message
|
|
188
|
+
id,
|
|
189
|
+
category,
|
|
190
|
+
status: overallStatus,
|
|
191
|
+
message,
|
|
192
|
+
details: {
|
|
193
|
+
totalPages,
|
|
194
|
+
testedEndpoints: results.length,
|
|
195
|
+
sampled,
|
|
196
|
+
passBucket,
|
|
197
|
+
warnBucket,
|
|
198
|
+
failBucket,
|
|
199
|
+
fetchErrors,
|
|
200
|
+
endpointResults: results,
|
|
201
|
+
discoveryWarnings: warnings,
|
|
202
|
+
},
|
|
8
203
|
};
|
|
9
204
|
}
|
|
10
205
|
registerCheck({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-header-hygiene.js","sourceRoot":"","sources":["../../../src/checks/observability/cache-header-hygiene.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"cache-header-hygiene.js","sourceRoot":"","sources":["../../../src/checks/observability/cache-header-hygiene.ts"],"names":[],"mappings":"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"}
|