-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path_headers
More file actions
274 lines (258 loc) · 18.2 KB
/
Copy path_headers
File metadata and controls
274 lines (258 loc) · 18.2 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
# Netlify custom response headers.
#
# X-Clacks-Overhead: GNU Terry Pratchett
#
# In Going Postal (2004) the clacks towers carry an Overhead — a working channel no
# customer sees. When John Dearheart died on a tower his father put his name into it
# behind three letters: G, pass it on; N, do not log it; U, turn it around at the end
# of the line. So the name keeps going, up and down the towers, for as long as the
# network stands. A man's not dead while his name is still spoken.
#
# Readers on the Discworld subreddit proposed the same thing for the network we
# actually have, in March 2015, days after Pratchett died on the 12th. It is a fan
# convention and not his instruction or his estate's — Zine No. 49, One Atom of
# Justice, says so on its face and explains the whole thing:
# https://starstuff.earth/one-atom-of-justice-zine.html
#
# This header does nothing. That is the point.
# ── Security response headers (added 2026-09-09, from a specification.website audit) ──
#
# Netlify already sends `strict-transport-security: max-age=31536000` on its own, and
# already redirects http -> https. The line below restates it WITH includeSubDomains,
# which Netlify's default omits. Checked before adding it: the only subdomain that
# resolves is www.starstuff.earth, which is on Netlify and 301s to the apex over
# HTTPS, so forcing HTTPS across the whole tree for a year costs nothing. Deliberately
# NO `preload` — the preload list's own operator now discourages it, and it is close
# to irreversible.
#
# nosniff stops a browser guessing a response's type when the Content-Type looks
# wrong. Cheap, and there is no case here where guessing helps.
#
# frame-ancestors is the clickjacking control, and it is the CSP form rather than the
# legacy X-Frame-Options. It is NOT 'none': stimpunks.org's /star-stuff/ hub is our
# single best referrer and morerealms.com is the other half of the masthead, so both
# collaborators can embed us. Everyone else cannot. If a partner ever needs to frame
# a page, add their origin here rather than dropping the directive.
#
# THE FULL CSP LANDED 2026-09-09 and is generated, not typed — see tools/build-csp.mjs.
# The note that used to sit here said a real policy needed either nonces, which static
# hosting cannot produce, or 'unsafe-inline', which would be a policy in name only. Both
# halves were true; the way out is the third option the spec names, which is hashes. The
# 49 distinct inline <script> bodies and 6 distinct inline event handlers on this site are
# each hashed into script-src, so an inline script an attacker injects does not run. The
# 'unsafe-inline' at the end of that directive is IGNORED by any browser that understands
# the hash list — CSP3 discards it whenever a hash or a nonce is present — and is there
# only as a fallback for browsers that do not.
#
# THAT IS VERIFIED RATHER THAN ASSUMED. The test is whether an injected inline <script> is
# refused by a current browser, and it is, with a violation logged. If that ever stops
# being true, 'unsafe-inline' has to come out, at the cost of the pager on Safari < 15.4.
#
# style-src DOES permit inline CSS, and that one is real rather than nominal: 2,290
# style="…" attributes carry the nav accents, the card colours and every twinkle position,
# and an attribute cannot be hashed without hashing all 2,290. CSS injection can deface
# and can leak some data through selectors; it cannot execute script. Said plainly here
# because privacy.html refuses, in public, to ship the reassuring half of a header.
#
# The region below is rewritten by the tool, so don't hand-edit it. A new page with a new
# inline script needs its hash added or that script will not run — on a zine that means a
# dead pager — which is why `node tools/build-csp.mjs --check` is a ship gate.
#
# Referrer-Policy: our URLs are public documents, so leaking one is not a privacy
# problem in itself — but a reader following an outbound citation should not hand the
# destination the full path they came from. strict-origin-when-cross-origin sends the
# bare origin off-site and the full path within it.
#
# Permissions-Policy names ONLY features nothing on this site uses. Autoplay,
# encrypted-media, fullscreen, picture-in-picture, clipboard-write, accelerometer,
# gyroscope and web-share are all left alone on purpose: 292 YouTube embeds depend on
# them and only 6 of those iframes carry an explicit `allow=`, so a document-level
# denial would quietly break the racks. Verified before writing this list.
/*
X-Clacks-Overhead: GNU Terry Pratchett
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
# >>> BEGIN GENERATED CSP — tools/build-csp.mjs, do not hand-edit <<<
Content-Security-Policy: default-src 'self'; base-uri 'none'; object-src 'none'; form-action 'none'; frame-ancestors 'self' https://stimpunks.org https://morerealms.com; frame-src https://www.youtube-nocookie.com https://open.spotify.com; img-src 'self'; font-src 'self'; connect-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'sha256-/2Hc+9mKe3YPBaOtSd6xbg7oe8fB//ts/W5yym4h4/s=' 'sha256-2Tbl0Z2ZvG4Ubgmr46HvEQ3Wh9NAV1OKR2ulxAQ1beI=' 'sha256-3XeNqzkC0mHiPL5ICJ3k/Lw15YNxPLM1mYFcDh3qu/o=' 'sha256-6E2sAryRCt/KPsS5C2gQ9yZ2OcnSWZJGm76v53Y2v3c=' 'sha256-8EoutgQTpwKqGKDKH/IuLzAnP+5OT8uuskYwbxJ6vlw=' 'sha256-9WAghdnPJrnSLWoX1+3sXDWGkd/Gm2qILuMh7Rk+x4M=' 'sha256-BmD/yvpbYdJG0IYrp7X390vMpWI0ZbOsMqfikUDrteo=' 'sha256-COe6/N3l8jLCC2rwfjDXm4ykcefV74LLZtHhQvzLko4=' 'sha256-CaSNuuZv7GpSLFtEQLF9jrMuNLyAaGzB4DpzNyDh/Ms=' 'sha256-FAxOm0r1XDsdD4S1zhM1CVHYy6xIYqYwBjupeJbUVko=' 'sha256-FRhMnoQ35nfgVyAqr625yliDS0wOYp7/AHa0uqYJSoQ=' 'sha256-MqL41yWjzlUU3AmUHB4OHe7/6wy2t/FzFzN/JcJMoZ8=' 'sha256-NScxUqQ0weX8fGBsbxCF2WElH5rEpbYHCFaJImrAIh0=' 'sha256-NWWNprNlGocUgauncK3WYoh2Td8X6VF9CNTdlqXaYNI=' 'sha256-NfqSKXlZhilDuXTcLBNA+yN7bpHKC+Ji4WFW7GJHMt0=' 'sha256-OycFq8kby8EO/TjXaeNX0uLMURcNDM5mnuc4sfa06oA=' 'sha256-P4cpTwZGn6kKjtFicuJh2bVh8JZne+R53RpDyE5ptDU=' 'sha256-Tb7L5dGY72KJuzcfa83hTzILj/J+eamq3/brqGNPhGE=' 'sha256-Tzx5sXzJ/ATUG+4Pw8NsKMFOg0Ri6kbhGmXGkh1A8xc=' 'sha256-WoEHrouQABAFij8bCfnX1b0ciqWDP5GOuLJlO04BrCQ=' 'sha256-WtKZdcdwH/5teyDueRqr/SYo/jNe814rDIaIwuscmvY=' 'sha256-X3DA0TnSO2oEMIno/b4XP1Et7VYb1LundY7zqC9LVs8=' 'sha256-YSblv+FVG0t6w8bljmU6tP5ADKmXXO9grxQs96BQlFk=' 'sha256-dhPPQWyFwZFcScDVpVCWklv4IL8NnabzNfXOqqz3njI=' 'sha256-eWWDXE424/xdzC915mGXQbSK66Xr/Cyj1yye9XpIkrI=' 'sha256-lkT1i/yvUNWBv3V0R3fYMdV7r+6SlpV4HpgYGtel5Yc=' 'sha256-n0VXoJCL6fj73VvHnSQm/IklCwCEhTj15173TEylr2k=' 'sha256-nOt6CV3EROzVHdLDDCZZdTp/ocda0WLqjmhpE0KTuso=' 'sha256-t7zySTRbeG53CVhE0K9jadC6QezsofM4XVhmaMuIYC4=' 'sha256-tH0bhn6YnOSy716OrUUpV/b2uyeOtSwEBl4ul2Vo5Uk=' 'sha256-tutnuiN0u/+SAHQ5ujWXHDRRSU9n1oI9hTEMjO17GFo=' 'sha256-u55zJyMEboc5gQrykADEwGh2BXZQetI9upUdLRZi6xE=' 'sha256-uQPcby2yrEAL3dKW78KZUzRcEF+AdsgSOGaX6sHQii8=' 'sha256-zUOnjpKaRPuCep1S7+I+33G4ZWJFHbAAhCZ/xlKHKlI=' 'unsafe-inline'; upgrade-insecure-requests
# >>> END GENERATED CSP <<<
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), midi=(), serial=(), bluetooth=(), display-capture=(), xr-spatial-tracking=()
Link: </llms.txt>; rel="describedby"; type="text/markdown"; title="Site index for language models", </feed.xml>; rel="alternate"; type="application/rss+xml"; title="Star Stuff", </changelog.xml>; rel="alternate"; type="application/rss+xml"; title="Star Stuff — Changelog", </.well-known/api-catalog>; rel="api-catalog"; type="application/linkset+json", <https://creativecommons.org/licenses/by-sa/4.0/>; rel="license"
# security.txt (2026-09-09), RFC 9116. text/plain is required by the RFC and Netlify
# infers it from the extension anyway; stated because a Content-Type is a claim and this
# one is cheap to make explicitly. Cached for a day rather than a week: the file carries
# an Expires date, and a stale copy in a proxy outliving its own validity window is the
# one caching failure that actually matters here.
#
# THE LINK HEADER NO LONGER ADVERTISES IT, and the reason is a correction. This comment
# used to say rel="security" "is IANA-registered". It is not: the IANA link-relations
# registry was fetched and read on 2026-09-10 — 236 entries — and neither `security` nor
# `sitemap` appears anywhere in it, though both were in the header above. RFC 8288 admits
# an extension relation only as a full URI, never as a bare token, so those two were not
# a lax choice but an invalid one, on every response for a day.
#
# Nothing is lost. A security.txt is found at its well-known path — that is what a
# well-known path is for — and robots.txt already carries `Sitemap:`, which is the
# canonical mechanism and the one every crawler actually reads. The expiry is gated in
# tools/build-derived.mjs: a lapsed security.txt is INVALID per the RFC rather than
# merely old, so it is checked on every ship instead of trusted to a calendar.
/.well-known/security.txt
Content-Type: text/plain; charset=utf-8
Cache-Control: public, max-age=86400, must-revalidate
# The per-page Markdown siblings (2026-09-09). 56 of the 198 pages have one; the other
# 142 deliberately do not — see tools/build-markdown.mjs for why a zine's Markdown would
# say less than the zine. Content-Type matters twice over here: without it a browser
# offers to download the file instead of showing it, and an agent may misclassify it.
#
# The per-page rel="alternate" lives in each page's own <head> rather than here, because
# it varies by path and a header cannot. The site-wide relations above do not vary.
#
# Cached like the markup it mirrors, not longer: a .md that outlives an edit to its page
# is the drift the generator exists to prevent, arriving through a proxy instead.
/*.md
Content-Type: text/markdown; charset=utf-8
Cache-Control: public, max-age=0, must-revalidate
# llms.txt (2026-09-09). Served as text/markdown, which is what the convention asks for
# and what makes a browser show it rather than offer to download it.
#
# The Link header above is the part that matters, and it is v2 of the convention's one
# hard addition: v1 expected an agent to GUESS /llms.txt at the root, so a file nobody
# advertised was found only by an agent that had already assumed the path. rel="describedby"
# replaces the guess. It rides on /* rather than on the HTML pages alone, deliberately —
# the whole point of a header over a <link> is that it reaches a client that never parses
# our HTML, which includes anything fetching feed.xml or search-index.json directly.
#
# Every rel there is IANA-registered: describedby, sitemap, alternate, license. Inventing
# one is a bad signal and crawlers ignore it. URIs go in angle brackets, not quotes.
/llms.txt
Content-Type: text/markdown; charset=utf-8
Cache-Control: public, max-age=3600, must-revalidate
# Images (2026-09-09, with the AVIF pass). Every raster is an AVIF -> WebP -> JPEG
# chain, and Content-Type is stated because the <source type> attribute is only how the
# browser CHOOSES a candidate — the response still has to arrive as an image, and a host
# that does not know .avif serves it as octet-stream. Cached the same way as the fonts and
# for the same reason: the filenames carry no content hash, so `immutable` would promise
# something the names cannot keep. If these ever get hashed names, raise it to a year.
# /.well-known/api-catalog (2026-09-10), RFC 9727, as an RFC 9264 Linkset. Generated by
# tools/build-derived.mjs and gated by its --check.
#
# THE CONTENT-TYPE IS LOAD-BEARING AND NETLIFY CANNOT GUESS IT. The file has no
# extension, so a static host serves it as application/octet-stream by default, and the
# spec's own mistake list says an agent that type-checks strictly will skip anything that
# is not application/linkset+json. This block is the whole reason the file works.
#
# Cached for an hour. It names four resources that change rarely, but it is also the one
# document an agent is most likely to have fetched before we changed something, and an
# hour is short enough that a wrong catalogue cannot outlive a deploy by much.
/.well-known/api-catalog
Content-Type: application/linkset+json; charset=utf-8
Cache-Control: public, max-age=3600
# Agent Skills discovery (2026-09-10), Cloudflare-led RFC draft v0.2.0. A SKILL.md is a
# short instruction file an agent can load to work with this site properly; the index
# names it and carries a sha256 of its bytes. Both generated-or-checked by
# tools/build-derived.mjs, which computes the digest from disk — a digest that has drifted
# from its artefact makes the skill unverifiable, and a compliant client will refuse it.
#
# CORS IS OPEN ON BOTH, DELIBERATELY, AND IT IS THE ONLY PLACE ON THIS SITE THAT IS.
# A browser-based agent fetching from another origin gets nothing without it, and these
# two files are the one thing here written to be read cross-origin. The risk is the
# ordinary one for a public static file that contains no secrets and no user data: none
# that a plain GET of the same URL does not already carry. Do not copy this block to
# anything else.
#
# THERE IS DELIBERATELY NO `Link: rel="agent-skills"` HEADER, and the spec's page asks for
# one. `agent-skills` is NOT in the IANA link-relations registry — checked the same day we
# removed `sitemap` and `security` from that header for exactly this reason — and RFC 8288
# admits an extension relation only as a full URI, never a bare token. Adding it would
# repeat, within the hour, the error we had just published a correction about. The
# well-known path IS the discovery mechanism the RFC defines, so nothing is lost but a
# validator tick. Revisit if the relation is ever registered.
/.well-known/agent-skills/index.json
Content-Type: application/json; charset=utf-8
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=3600
/.well-known/agent-skills/*/SKILL.md
Content-Type: text/markdown; charset=utf-8
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=3600
# site.webmanifest (2026-09-10). application/manifest+json is required for installability
# — served as text/html or application/json, Chromium silently declines to install and
# reports nothing a reader would see. Generated by tools/build-derived.mjs, which reads
# the theme colour off the pages and asserts each icon exists rather than trusting a
# hand-kept list.
/site.webmanifest
Content-Type: application/manifest+json; charset=utf-8
Cache-Control: public, max-age=3600
# The raster icons (2026-09-10). Derived from favicon.svg by tools/build-icons.mjs, and
# immutable in practice: the mark has not changed since the site opened, and a launcher
# or home-screen icon that lags a rebrand by a week costs nothing, where a favicon
# re-fetched on every navigation costs every reader. A week, not a year, because there is
# deliberately no content hash in these filenames — the spec requires favicon.ico and
# apple-touch-icon.png at exactly those root paths, so they cannot be fingerprinted.
/favicon.ico
Cache-Control: public, max-age=604800
/apple-touch-icon.png
Cache-Control: public, max-age=604800
/icon-192.png
Cache-Control: public, max-age=604800
/icon-512.png
Cache-Control: public, max-age=604800
/icon-maskable-512.png
Cache-Control: public, max-age=604800
/*.avif
Cache-Control: public, max-age=604800, must-revalidate
Content-Type: image/avif
/*.webp
Cache-Control: public, max-age=604800, must-revalidate
Content-Type: image/webp
/*.jpg
Cache-Control: public, max-age=604800, must-revalidate
# Self-hosted webfonts (2026-09-09). Fonts moved off fonts.gstatic.com so a page view
# stops handing a reader's IP to a third party; see the block at the top of starstuff.css.
#
# NOT `immutable`. The filenames carry the family, style, weight range and unicode subset
# but no content hash, so a future re-download from google/fonts would reuse a filename for
# different bytes — and immutable tells a browser never to ask again for up to a year. A
# week of caching with must-revalidate afterwards is the honest trade for an unhashed asset,
# and these are ~10-80 KB files a reader fetches once. If they ever get content-hashed
# names, this is the line to change to max-age=31536000, immutable.
#
# Content-Type is stated because Netlify does not always infer woff2, and a font served as
# octet-stream is refused by the font loader with nothing useful in the console.
/fonts/*
Cache-Control: public, max-age=604800, must-revalidate
Content-Type: font/woff2
# The OFL licence texts that ship beside the fonts, as required by the licence. Plain text.
/fonts/OFL-*.txt
Cache-Control: public, max-age=604800
Content-Type: text/plain; charset=utf-8
# edit.js (2026-09-13). NOT SHIPPED TO READERS, and it is listed here precisely because
# that is the easy thing to forget: no page on this site loads it, it arrives from a
# bookmark, and an asset with no cache policy on a file that states one for every other
# asset reads as an oversight rather than as a decision.
#
# max-age=0 with must-revalidate, like the markup and the .md siblings rather than like
# the fonts. Only the handful of people holding the bookmark ever fetch it, once per
# editing session, so the saving from caching it is nil — and the cost is not. The
# refusals ARE the file: which quotations, citations, epistemic grades and cards it
# declines to touch. A week-old copy is a copy whose refusals are last week's, and the
# failure mode is a contributor rewording something the current version would have
# stopped. That is the wrong side to be stale on.
#
# Content-Type stated for the reason it is stated on the fonts: Netlify infers .js
# correctly today, and a script served as octet-stream is refused by script-src with
# nothing useful in the console.
/edit.js
Content-Type: text/javascript; charset=utf-8
Cache-Control: public, max-age=0, must-revalidate
# The RSS feed. Netlify serves .xml as application/xml, which every reader accepts,
# but application/rss+xml is what a feed actually is — and it is what makes a browser
# offer to subscribe rather than dump the markup on screen. Charset stated explicitly:
# the document declares UTF-8 in its prolog, and a header that disagreed would win.
/feed.xml
Content-Type: application/rss+xml; charset=utf-8
# The changelog's own feed (2026-09-21). Same reasoning, separate document: feed.xml
# carries the pieces and this one carries why they changed, which is the split
# whats-new.html and changelog.html already make on the page side.
/changelog.xml
Content-Type: application/rss+xml; charset=utf-8