-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path_headers
More file actions
338 lines (308 loc) · 21.7 KB
/
Copy path_headers
File metadata and controls
338 lines (308 loc) · 21.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# Netlify custom response headers.
/*
# ── Caching ──────────────────────────────────────────────────────────────────
# This is the spec's HTML recommendation VERBATIM, and it was already what Netlify
# sent — the HTML half of the audit's cache finding was never wrong. It is written
# out here so it is a decision we own rather than a default we inherited, and so the
# overrides below have something explicit to override.
#
# It also covers the .md mirrors, which are content and should revalidate with it.
#
# NO `stale-if-error` HERE. RFC 9111 forbids serving a stale `must-revalidate`
# response, so the two directives cancel and pairing them is a mistake the spec
# calls out by name. Resilience goes on the responses below that are not
# must-revalidate.
Cache-Control: public, max-age=0, must-revalidate
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
# Clickjacking. Added 2026-09-09 from the spec audit, which found the site sending
# no embedding policy at all — no CSP, no X-Frame-Options, from here or from
# Netlify's defaults.
#
# The usual argument for this header is session theft, and it does not apply: there
# is no login here, no form, and no authenticated action to hijack. The reason it
# matters on THIS site is attribution. Without it anybody can frame a sheet inside
# their own chrome and present it as theirs, on a site whose stated correctness
# requirement is that credit reaches the person it belongs to.
#
# THE POLICY BELOW IS GENERATED, AND THE HASH IS WHY. It names a sha256 of the inline
# snippet every page runs before first paint. That snippet lists the nine typefaces the
# picker offers, so narrowing the picker changes it — and a stale hash does not warn, it
# makes the browser refuse the snippet on every page, so every reader who asked for the
# cabinet or for plain view gets the other thing flashed at them. That is the exact
# failure the note above describes, arriving silently.
#
# This carried `frame-ancestors 'none'` and nothing else until 2026-09-13, on the stated
# grounds that a script-src would kill the inline snippet. Measured rather than assumed
# when the question was reopened: there is exactly ONE executable inline script on the
# site and it is byte-identical on all 29 pages, so one hash covers everything. The 28
# JSON-LD blocks need none — a data block is never executed.
#
# PROVED IN A BROWSER BEFORE IT SHIPPED, BOTH WAYS. The policy was pasted into a copy of
# the home page as a meta tag and loaded with a preference stored: the snippet ran and the
# cabinet came up. The same copy with one character changed in the hash refused it, and
# Chrome's own message named `sha256-5Qwf5d0QAc80k5TT+DSHHcYwQpIqpBQd394Ya69YWh8=` as the
# hash it wanted — the browser confirming the generator's arithmetic rather than us.
#
# `style-src` keeps 'unsafe-inline' because 1,299 inline style attributes carry each
# sheet's foxing seed, stamp rotation and card colours, and CSP hashes do not cover style
# ATTRIBUTES, only <style> elements. The alternative is not a stricter policy, it is
# deleting the per-page palette.
#
# Run `node tools/make-csp.mjs` after anything that changes an inline script.
# `check-cache.mjs` fails if this line and the pages disagree.
# >>> csp — generated by tools/make-csp.mjs; do not hand-edit this line
Content-Security-Policy: default-src 'self'; base-uri 'none'; object-src 'none'; form-action 'none'; frame-ancestors 'none'; img-src 'self'; font-src 'self'; connect-src 'self'; frame-src https://www.youtube-nocookie.com; style-src 'self' 'unsafe-inline'; script-src 'self' 'sha256-5Qwf5d0QAc80k5TT+DSHHcYwQpIqpBQd394Ya69YWh8='
# <<< csp
X-Frame-Options: DENY
# ── What a page here is allowed to ask the device for ────────────────────────
# Added 2026-09-13, the same day the real CSP went up and for the same reason: the
# site sent no permissions policy at all, and neither did Netlify's defaults.
#
# THIS SITE CALLS NO GATED FEATURE OF ITS OWN — checked, not assumed. There is no
# getUserMedia, no geolocation, no requestFullscreen, no payment, no WebUSB, no
# navigator.share anywhere in our scripts. So every denial below costs a reader
# nothing and closes a surface that could only ever be opened by something we did
# not write.
#
# THE FIVE DELEGATIONS ARE THE WHOLE OF THE THOUGHT HERE. An iframe's `allow`
# attribute can only NARROW what the page already has; it cannot grant what this
# header denies. `queering-embed.js` builds its player with
# `allow="accelerometer; autoplay; encrypted-media; picture-in-picture; fullscreen"`
# and a `?autoplay=1` URL, so a blanket denial would have left a reader who pressed
# the facade with a player that would not start and a fullscreen button that did
# nothing — a real control broken to tidy a header. They are delegated to the one
# origin, never to `*`, and only `fullscreen` also keeps `self`.
#
# `clipboard-write` IS NOT NAMED HERE AND THAT IS DELIBERATE. Both injected layers
# copy out through `navigator.clipboard.writeText`, so it looked like the thing most
# in need of protecting — but it is NOT a directive in the Permissions Policy
# registry. Writing it would have been the `sitemap` link relation again: a token
# that reads as standard, is not, and rides along on every response saying nothing.
# Verified against the directive list rather than remembered.
#
# WHAT IS DELIBERATELY LEFT UNNAMED, because denying it could cost a reader
# something and protects nobody: `aria-notify`, which fires screen-reader
# announcements; the translation, summarisation and speech APIs, which are a
# reader's own tools for getting at our words; and the cross-origin-isolation and
# fetch-quota plumbing, which is not a capability anybody asks a device for.
# A deny-everything list would have looked more careful and been less so.
Permissions-Policy: accelerometer=("https://www.youtube-nocookie.com"), autoplay=("https://www.youtube-nocookie.com"), encrypted-media=("https://www.youtube-nocookie.com"), picture-in-picture=("https://www.youtube-nocookie.com"), fullscreen=(self "https://www.youtube-nocookie.com"), ambient-light-sensor=(), attribution-reporting=(), bluetooth=(), browsing-topics=(), camera=(), captured-surface-control=(), ch-ua-high-entropy-values=(), compute-pressure=(), display-capture=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), identity-credentials-get=(), idle-detection=(), local-fonts=(), local-network=(), loopback-network=(), magnetometer=(), microphone=(), midi=(), otp-credentials=(), payment=(), private-state-token-issuance=(), private-state-token-redemption=(), publickey-credentials-create=(), publickey-credentials-get=(), screen-wake-lock=(), serial=(), speaker-selection=(), storage-access=(), usb=(), web-share=(), window-management=(), xr-spatial-tracking=()
# HSTS. Netlify's default was max-age=31536000 with no includeSubDomains; the spec
# asks for two years and the subdomain flag, so this now overrides it.
#
# The spec's caution is to audit subdomains first, because an HTTP-only subdomain
# breaks the moment this ships. Audited 2026-09-09: www.queering.earth answers 301
# to https://queering.earth/, so it is HTTPS-capable, and there is nothing else
# under the name. Recheck this before adding any subdomain.
#
# NO `preload`. The preload list's own operator now advises against it, removal
# takes months, and it applies to every subdomain. Do not add it.
Strict-Transport-Security: max-age=63072000; includeSubDomains
# ── Machine-readable discovery ───────────────────────────────────────────────
# RFC 8288 Link header, so an agent that never parses our HTML still finds the
# index, the manifest, and the licence. Registered relation types only — the IANA
# registry is the source of truth and inventing a rel is a bad signal.
#
# THAT SENTENCE WAS FALSE FOR AS LONG AS IT HAD BEEN WRITTEN. Checked against the
# registry on 2026-09-11 — all 236 entries, fetched, not remembered — and `sitemap`
# is not in it. RFC 8288 admits an extension relation only as a FULL URI, never as a
# bare token, so it was not a lax choice but an invalid one, riding on every response
# since the header shipped. `sitemap` is exactly the kind of token that reads as
# standard and is not.
#
# NOTHING IS LOST BY DROPPING IT. robots.txt carries `Sitemap:`, which is the
# canonical mechanism every crawler already reads, and sitemap.xml is named there.
# A second, invalid pointer bought nothing.
#
# `agent-skills` WENT THE SAME WAY, AND IT WAS THE ONE NEARLY MISSED. The note that
# prompted this check named only `sitemap`; the registry sweep found two. It is not a
# registered relation, and unlike security.txt's path it is not a registered
# well-known URI either — so the usual "a well-known path is self-discovering"
# argument is weaker here, and was weighed rather than assumed. It went anyway, on
# Ryan's call: NOTHING STANDARD CONSUMES IT. An agent implementing the convention
# reads /.well-known/agent-skills/index.json by path, and would never look for this
# rel, because this rel is not a standard either. An invalid relation that nothing
# consumes is decoration asserting a fact — a gold join on a sheet nobody corrected,
# in the header that tells machines what to trust about this site.
#
# THE SKILL ITSELF IS UNCHANGED and still sits at that path, still named in the
# register and in /llms.txt's own prose. What went is a pointer no client asked for.
# rel=describedby on /llms.txt is llms.txt v2's one hard requirement: without it an
# agent is back to guessing the root path. Per-page `rel=alternate` for the .md
# source lives in each page's <head>, because it varies by path and this does not.
Link: </llms.txt>; rel="describedby"; type="text/markdown", </llms-full.txt>; rel="alternate"; type="text/markdown"; title="Full text", </feed.xml>; rel="alternate"; type="application/rss+xml"; title="What has arrived", </register.xml>; rel="alternate"; type="application/rss+xml"; title="The accession register", <https://creativecommons.org/licenses/by-sa/4.0/>; rel="license"
# TDM reservation (RFC-adjacent, W3C TDMRep). 0 means: mine it freely.
# The header rather than /.well-known/tdmrep.json because the answer does not vary
# by path, and the header covers responses with no <head> to put a <meta> in — the
# plates, the .md files, the sitemap. The spec is explicit that the well-known file
# is the weakest of the three mechanisms and is for path-varying answers.
# A reservation of 0 beside a CC BY-SA licence and Content-Signal: ai-train=yes is
# the consistent triple; a 1 here would contradict both.
tdm-reservation: 0
# The two index files are Markdown, and the Link header says so. Netlify types a .txt
# as text/plain, which the llms.txt spec allows — but declaring text/markdown in the
# Link header while serving text/plain is a disagreement an agent has to resolve, and
# the file genuinely is Markdown. The name is fixed by the convention; the type is not.
/llms.txt
Content-Type: text/markdown; charset=UTF-8
Cache-Control: public, max-age=600, stale-if-error=604800
/llms-full.txt
Content-Type: text/markdown; charset=UTF-8
Cache-Control: public, max-age=600, stale-if-error=604800
# ── The plates: thirty days, and NO `immutable` ────────────────────────────────
# 2.4 MB of scanned lithographs was being revalidated on every visit, which is the
# real cost the audit found. Thirty days captures essentially all of the repeat-visit
# benefit on a site this age.
#
# `immutable` is deliberately absent, and this is the interesting half. These bodies
# will almost certainly never change — the filename names a specific plate in a
# specific volume, and tab 4 of the 1863 Annales is not going to be re-cut. But the
# spec is explicit that `immutable` is for URLs carrying a content hash, and these do
# not. Promising a browser that a body can never change, on a filename that could be
# overwritten by a better scan, is asserting a fact we cannot enforce — the same
# failure as a gold seam on a sheet nobody corrected. Thirty days is a claim we can
# keep; a year with `immutable` is one we would only be hoping to.
#
# `stale-if-error` works here because there is no `must-revalidate` to cancel it: a
# Netlify incident leaves the plates showing rather than broken.
/images/*
Cache-Control: public, max-age=2592000, stale-while-revalidate=604800, stale-if-error=2592000
# ── The stylesheet and the script: five minutes, and no fingerprint ────────────
# 27 KB and 4 KB over the wire, brotli'd. Both are the case the spec has no good answer
# for: not fingerprinted, because there is no build step, so `immutable` is unavailable
# and a long `max-age` would strand a reader on a stale sheet after a palette change
# with no way to bust it. That is this site's characteristic failure — Star Stuff
# shipped 44 pages that printed blank — so the bound matters more than the bytes.
#
# Five minutes is chosen for the shape of a visit rather than the size of the file: a
# reader moving through several sheets in one sitting revalidates once instead of once
# per sheet, and a bad palette reaches everyone within five minutes.
#
# THE STYLESHEET SHARES THE HTML'S POLICY BECAUSE IT SHARES THE HTML'S FATE, and this
# replaced `max-age=300` on 2026-09-09 after the five-minute window fired for real: the
# accession stamp shipped, and a reader with the previous stylesheet still cached got the
# new markup with none of its rules — a `p` of three unstyled spans where an oval should
# have been. The comment that used to sit here PREDICTED that exact event and priced it as
# acceptable. It is not: a silent styling failure is this site's characteristic failure,
# which is why the HTML revalidates in the first place, and the markup and the rule that
# styles it ship in one commit.
#
# Fingerprinting is still refused for the reason DECISIONS.md gives — it would churn twelve
# HTML files per CSS edit and make the diff of a one-line palette fix unreadable, and the
# register depends on those diffs being legible. This is the option that entry did not
# weigh, and it dominates: no generator, no rewritten pages, no gate, and the skew window
# closes to zero rather than to five minutes. The cost is one conditional request per visit
# that comes back 304 with an empty body, issued alongside the HTML's own revalidation.
#
# NO `stale-if-error` and NO `stale-while-revalidate`, for two reasons that agree. The
# first is mechanical: RFC 9111 forbids serving a stale `must-revalidate` response, so the
# pairing cancels — see the note at the head of this file. The second is why matching the
# HTML is coherent rather than merely tidy: the HTML is `must-revalidate` and fails closed,
# so when the origin cannot be reached there is no document to style, and a stale
# stylesheet held back for that case would have nothing to apply itself to. These fail
# exactly when the page they belong to fails.
/queering.css
Cache-Control: public, max-age=0, must-revalidate
/queering.js
Cache-Control: public, max-age=0, must-revalidate
# ── The search index ──────────────────────────────────────────────────────────
# 357 KB uncompressed and brotli'd to a fraction of it, fetched only by /search and
# only once a reader types. DERIVED FROM THE PAGES, which is the whole problem with
# caching it: this file's own rule is that staleness is safe only where the stale copy
# cannot disagree with the fresh HTML, and an index built from the sheets disagrees with
# them the moment one is edited. At `max-age=600` a searcher could be handed a snippet
# quoting a sentence that is no longer on the page it cites — on a site whose one stated
# correctness requirement is attribution, that is the wrong ten minutes to save.
#
# `max-age=0` AND NOT `must-revalidate`, and the difference is the point. Zero means a
# cache revalidates before reusing it, so there is no skew in normal operation. Leaving
# `must-revalidate` off is what keeps `stale-if-error` legal, and here that resilience is
# worth having where it is not for the stylesheet: this file is fetched AFTER the page has
# already rendered, by a reader who has typed something. The document exists either way,
# so a finding aid a few minutes behind really is better than one that fails closed —
# which is the judgement the previous note made, and it survives intact.
/search-index.json
Content-Type: application/json; charset=UTF-8
Cache-Control: public, max-age=0, stale-if-error=604800
# Loaded by /search alone, and it clones templates authored in that page — so it is
# skewable against the markup exactly the way the stylesheet is, and shares its fate.
/queering-search.js
Cache-Control: public, max-age=0, must-revalidate
# queering-embed.js reads data-embed-id and data-embed-title out of index.html, so it is
# markup-coupled for exactly the reason the other two scripts are: a stale copy could point
# the player at a video the page no longer names. Same policy, same fate.
/queering-embed.js
Cache-Control: public, max-age=0, must-revalidate
# ── The faces ─────────────────────────────────────────────────────────────────
# Self-hosted since 2026-09-09 so that reading a page makes no third-party request. The type is
# set explicitly rather than trusted: production sends nosniff, and a font served as
# application/octet-stream does not render — the same trap the .avif plates nearly
# walked into.
#
# A year, and still no `immutable`. These files are a specific cut of a specific
# release, so a year is a claim we can keep; but the name carries no content hash, so
# the promise that the body can NEVER change is one we would only be hoping to keep.
# Same reasoning as the plates.
# NO Content-Type HERE. It said `font/woff2` for everything under /fonts/, which was
# true when only woff2 lived here and became a lie the moment the picker's OTFs
# arrived — and production sends nosniff, so a wrong font type is one the page cannot
# recover from. Netlify types both correctly on its own: font/otf and font/woff2,
# verified against files that carry no override. The third time this trap has been
# hit here, and the first time by asserting a type rather than by omitting one.
/fonts/*
Cache-Control: public, max-age=31536000, stale-if-error=2592000
Access-Control-Allow-Origin: *
# The specimen is a scratch page for choosing a font picker's candidates. Typed
# explicitly for the same reason /fonts/* is: nosniff means a type Netlify guesses
# wrong is one the page cannot recover from, and six of the candidates are OTF.
# DELETE THIS BLOCK WITH THE FOLDER once the choice is made.
/specimen/fonts/*
Cache-Control: public, max-age=604800
Access-Control-Allow-Origin: *
# The marks are regenerated by tools/make-images.py, so they are not immutable either.
# A week: long enough that nobody re-fetches them, short enough that a redrawn mark
# arrives without anybody clearing a cache.
/favicon.svg
Cache-Control: public, max-age=604800, stale-if-error=2592000
/favicon.ico
Cache-Control: public, max-age=604800, stale-if-error=2592000
/apple-touch-icon.png
Cache-Control: public, max-age=604800, stale-if-error=2592000
/images/icon-192.png
Cache-Control: public, max-age=604800, stale-if-error=2592000
/images/icon-512.png
Cache-Control: public, max-age=604800, stale-if-error=2592000
/images/icon-maskable-512.png
Cache-Control: public, max-age=604800, stale-if-error=2592000
# The web app manifest. TYPED EXPLICITLY BECAUSE INSTALLABILITY FAILS SILENTLY:
# a manifest served as text/plain or text/html is simply ignored, with no console
# error and no sign on the page. Same week as the icons, for the same reason — it
# names them, and a redrawn mark should not be pinned behind a stale manifest.
/site.webmanifest
Content-Type: application/manifest+json; charset=UTF-8
Cache-Control: public, max-age=604800, stale-if-error=2592000
# Manifests and policy files. They change when content changes, and they are exactly
# the responses the spec names for `stale-if-error`.
/sitemap.xml
Cache-Control: public, max-age=600, stale-if-error=604800
/robots.txt
Cache-Control: public, max-age=600, stale-if-error=604800
# TWO FEEDS, TWO LISTS. /feed.xml is the pages as they arrive; /register.xml is the
# register's own accessions, corrections included. Same policy: both are fetched by a
# reader's client long after any page has rendered, so neither can contradict markup
# that is already on screen, and both take stale-if-error rather than must-revalidate.
/feed.xml
Cache-Control: public, max-age=600, stale-if-error=604800
/register.xml
Cache-Control: public, max-age=600, stale-if-error=604800
# The draft review layer. Dormant on the published site — no page here references it, and
# check-metadata.mjs check 10 is what keeps that true — but it is served, so it says plainly
# that nothing under this path is for an index. Cache-Control is inherited from /*, which is
# correct: it reads a draft's own markup and must not outlive it.
/drafts/*
X-Robots-Tag: noindex
# Agent Skills discovery. The spec asks for CORS wide open on both files so a
# browser-based agent can fetch them; they are a public description of a public site.
/.well-known/agent-skills/*
Access-Control-Allow-Origin: *