@jimhoyd/urlcode 0.4.2 → 0.4.6

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.
Files changed (214) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +57 -14
  2. package/.claude/skills/urlcode-operations/SKILL.md +4 -0
  3. package/README.md +19 -15
  4. package/SECURITY.md +5 -3
  5. package/dist/BUILD-MANIFEST.json +30 -27
  6. package/dist/agent-context.js +82 -0
  7. package/dist/agents-guide.js +42 -42
  8. package/dist/authoring.js +12 -2
  9. package/dist/body-schema.js +159 -0
  10. package/dist/build-cloudflare.js +2 -0
  11. package/dist/capabilities.js +1 -1
  12. package/dist/cli.js +28 -12
  13. package/dist/config.js +74 -6
  14. package/dist/context.js +4 -6
  15. package/dist/errors.js +3 -1
  16. package/dist/examples.js +1 -1
  17. package/dist/extensions.js +60 -2
  18. package/dist/http-policy.js +19 -4
  19. package/dist/http-response.js +2 -2
  20. package/dist/init-with.js +71 -9
  21. package/dist/mcp.js +15 -2
  22. package/dist/pattern-guard.js +32 -0
  23. package/dist/policies/security.js +0 -0
  24. package/dist/project-tests.js +35 -11
  25. package/dist/readiness.js +184 -32
  26. package/dist/recipes.js +1 -1
  27. package/dist/router.js +17 -0
  28. package/dist/runtime.js +26 -3
  29. package/dist/scaffold.js +0 -0
  30. package/dist/server.js +26 -3
  31. package/dist/site.js +0 -0
  32. package/dist/tooling.js +2 -1
  33. package/dist/types/agent-context.d.ts +44 -0
  34. package/dist/types/authoring.d.ts +3 -1
  35. package/dist/types/body-schema.d.ts +58 -0
  36. package/dist/types/config.d.ts +10 -2
  37. package/dist/types/context.d.ts +1 -1
  38. package/dist/types/errors.d.ts +9 -1
  39. package/dist/types/examples.d.ts +1 -1
  40. package/dist/types/extensions.d.ts +47 -2
  41. package/dist/types/http-policy.d.ts +3 -0
  42. package/dist/types/init-with.d.ts +10 -1
  43. package/dist/types/pattern-guard.d.ts +10 -0
  44. package/dist/types/project-tests.d.ts +9 -0
  45. package/dist/types/readiness.d.ts +68 -0
  46. package/dist/types/recipes.d.ts +1 -1
  47. package/dist/types/runtime.d.ts +4 -0
  48. package/dist/types/server.d.ts +9 -1
  49. package/dist/types/tooling.d.ts +1 -0
  50. package/dist/types/types.d.ts +13 -0
  51. package/dist/types.js +10 -2
  52. package/dist/typescript-authoring.js +5 -3
  53. package/dist/verify-deployment.js +16 -10
  54. package/examples/body-validation/README.md +16 -0
  55. package/examples/body-validation/example.yaml +17 -0
  56. package/examples/body-validation/tests/requests.json +15 -0
  57. package/examples/body-validation/urlcode.yaml +34 -0
  58. package/examples/coverage-waiver/README.md +8 -0
  59. package/examples/coverage-waiver/example.yaml +16 -0
  60. package/examples/coverage-waiver/functions/notes.mjs +2 -0
  61. package/examples/coverage-waiver/tests/requests.json +3 -0
  62. package/examples/coverage-waiver/urlcode.yaml +10 -0
  63. package/examples/data-dir/README.md +39 -0
  64. package/examples/data-dir/data/welcome.txt +1 -0
  65. package/examples/data-dir/example.yaml +22 -0
  66. package/examples/data-dir/functions/note.mjs +18 -0
  67. package/examples/data-dir/tests/requests.json +6 -0
  68. package/examples/data-dir/urlcode.yaml +6 -0
  69. package/examples/lifecycle/README.md +15 -0
  70. package/examples/lifecycle/example.yaml +19 -0
  71. package/examples/lifecycle/functions/notes.mjs +29 -0
  72. package/examples/lifecycle/tests/requests.json +12 -0
  73. package/examples/lifecycle/urlcode.yaml +29 -0
  74. package/examples/not-found/README.md +10 -0
  75. package/examples/not-found/example.yaml +17 -0
  76. package/examples/not-found/public/404.html +3 -0
  77. package/examples/not-found/public/index.html +3 -0
  78. package/examples/not-found/tests/requests.json +7 -0
  79. package/examples/not-found/urlcode.yaml +7 -0
  80. package/examples/shared-blocks/README.md +11 -0
  81. package/examples/shared-blocks/example.yaml +17 -0
  82. package/examples/shared-blocks/tests/requests.json +7 -0
  83. package/examples/shared-blocks/urlcode.yaml +37 -0
  84. package/llms-full.txt +364 -49
  85. package/llms.txt +43 -5
  86. package/package.json +15 -22
  87. package/recipes/static-page/README.md +9 -0
  88. package/recipes/static-page/public/index.html +11 -0
  89. package/recipes/static-page/recipe.yaml +21 -0
  90. package/recipes/static-page/tests/requests.json +22 -0
  91. package/recipes/static-page/urlcode.yaml +7 -0
  92. package/recipes/static-plus-api/README.md +6 -0
  93. package/recipes/static-plus-api/urlcode.yaml +4 -0
  94. package/recipes/store-crud/README.md +53 -0
  95. package/recipes/store-crud/recipe.yaml +31 -0
  96. package/recipes/store-crud/tests/requests.json +18 -0
  97. package/recipes/store-crud/urlcode.yaml +18 -0
  98. package/schemas/urlcode.schema.json +125 -60
  99. package/skills/urlcode/SKILL.md +47 -27
  100. package/starters/default/AGENTS.md +43 -43
  101. package/starters/page/README.md +14 -0
  102. package/starters/page/public/index.html +12 -0
  103. package/starters/page/tests/requests.json +17 -0
  104. package/starters/page/urlcode.yaml +6 -0
  105. package/.claude-plugin/marketplace.json +0 -18
  106. package/CONTRIBUTING.md +0 -148
  107. package/ROADMAP.md +0 -70
  108. package/docs/AI-AUTHORING.md +0 -339
  109. package/docs/ASSETS.md +0 -107
  110. package/docs/AUTH-BACKUP.md +0 -32
  111. package/docs/AWS.md +0 -86
  112. package/docs/BEST-PRACTICES.md +0 -276
  113. package/docs/BULK.md +0 -79
  114. package/docs/CAPABILITIES.md +0 -192
  115. package/docs/CAPACITY.md +0 -305
  116. package/docs/CI-FOLLOWUP-2026-09-19.md +0 -97
  117. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +0 -322
  118. package/docs/CI.md +0 -147
  119. package/docs/CLOUDFLARE.md +0 -109
  120. package/docs/CODEBASE-AUDIT-2026-09-20.md +0 -284
  121. package/docs/COMPLIANCE.md +0 -239
  122. package/docs/COMPOSING-A-SITE.md +0 -287
  123. package/docs/CONDITIONS.md +0 -74
  124. package/docs/CONTAINER-PROMOTION.md +0 -74
  125. package/docs/DEPLOYMENT-CHECKS.md +0 -108
  126. package/docs/DEVELOPMENT-PIPELINE.md +0 -304
  127. package/docs/EGRESS.md +0 -125
  128. package/docs/EXTENSIONS.md +0 -403
  129. package/docs/FRAMEWORK.md +0 -217
  130. package/docs/FUNCTION-SECURITY.md +0 -251
  131. package/docs/HTTP.md +0 -129
  132. package/docs/INSTALL.md +0 -128
  133. package/docs/INTERCHANGE.md +0 -134
  134. package/docs/LOAD-TESTING.md +0 -91
  135. package/docs/LOCAL-DEVELOPMENT.md +0 -102
  136. package/docs/MIDDLEWARE-EXAMPLES.md +0 -75
  137. package/docs/MIDDLEWARE.md +0 -102
  138. package/docs/MONITORING.md +0 -115
  139. package/docs/OBSERVABILITY.md +0 -222
  140. package/docs/OPEN-DECISIONS.md +0 -149
  141. package/docs/OPERATIONAL-PROOF.md +0 -41
  142. package/docs/OPERATIONS.md +0 -201
  143. package/docs/ORGANIZATION.md +0 -135
  144. package/docs/PERFORMANCE.md +0 -72
  145. package/docs/PLUGINS.md +0 -271
  146. package/docs/POLICIES.md +0 -211
  147. package/docs/PRERENDER.md +0 -245
  148. package/docs/PROJECT-DIRECTION.md +0 -118
  149. package/docs/PROVIDER-VERIFICATION.md +0 -84
  150. package/docs/READINESS.md +0 -153
  151. package/docs/README.md +0 -93
  152. package/docs/RECIPES.md +0 -99
  153. package/docs/RELEASE-0.4.0-alpha.3.md +0 -50
  154. package/docs/RELEASE-0.4.1.md +0 -73
  155. package/docs/RELEASE-0.4.2.md +0 -30
  156. package/docs/RELEASE-READINESS.md +0 -146
  157. package/docs/RELEASE-SECURITY.md +0 -100
  158. package/docs/RESILIENCE.md +0 -161
  159. package/docs/ROUTING.md +0 -92
  160. package/docs/SANDBOX-REVIEW.md +0 -72
  161. package/docs/SCAFFOLDING.md +0 -70
  162. package/docs/SECURITY-AUDIT.md +0 -164
  163. package/docs/SITE.md +0 -150
  164. package/docs/SPECIFICATION.md +0 -359
  165. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +0 -288
  166. package/docs/SPIKE-BUSINESS-SUITE.md +0 -1029
  167. package/docs/SPIKE-CORE-LAYERING.md +0 -368
  168. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +0 -207
  169. package/docs/STANDARDS.md +0 -311
  170. package/docs/STARTERS.md +0 -83
  171. package/docs/STATIC.md +0 -105
  172. package/docs/TOOLING.md +0 -298
  173. package/docs/TUNNELS.md +0 -72
  174. package/docs/TYPESCRIPT-AUTHORING.md +0 -87
  175. package/docs/TYPESCRIPT.md +0 -123
  176. package/docs/VERCEL.md +0 -114
  177. package/docs/VERSION-ALIGNMENT.md +0 -88
  178. package/docs/YAML-GUIDE.md +0 -57
  179. package/docs/YAML-REFERENCE.md +0 -449
  180. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +0 -68
  181. package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +0 -102
  182. package/docs/archive/2026-09-19/NEXT-PHASE-PLAN.md +0 -108
  183. package/docs/archive/2026-09-19/NEXT-STEPS.md +0 -646
  184. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +0 -277
  185. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +0 -186
  186. package/docs/archive/2026-09-19/ROADMAP.md +0 -387
  187. package/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md +0 -430
  188. package/docs/archive/2026-09-19/SPIKE-EXTENSIONS.md +0 -492
  189. package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +0 -365
  190. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +0 -778
  191. package/docs/archive/2026-09-19/USABILITY-REVIEW.md +0 -139
  192. package/docs/archive/2026-09-20/OPEN-DECISIONS-COMPLETED.md +0 -116
  193. package/docs/archive/README.md +0 -29
  194. package/docs/policies/agents.md +0 -182
  195. package/docs/policies/cache.md +0 -152
  196. package/docs/policies/compression.md +0 -169
  197. package/docs/policies/contract.md +0 -52
  198. package/docs/policies/hardened.md +0 -56
  199. package/docs/policies/interoperability.md +0 -169
  200. package/docs/policies/operations.md +0 -45
  201. package/docs/policies/security.md +0 -161
  202. package/docs/policies/throttle.md +0 -103
  203. package/docs/yaml/assets.md +0 -36
  204. package/docs/yaml/conditions.md +0 -20
  205. package/docs/yaml/functions.md +0 -168
  206. package/docs/yaml/middleware.md +0 -31
  207. package/docs/yaml/organization.md +0 -74
  208. package/docs/yaml/policies.md +0 -37
  209. package/docs/yaml/redirects.md +0 -64
  210. package/docs/yaml/responses.md +0 -57
  211. package/docs/yaml/site.md +0 -24
  212. package/packaging/claude-plugin/.claude-plugin/plugin.json +0 -19
  213. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +0 -130
  214. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +0 -108
package/docs/STANDARDS.md DELETED
@@ -1,311 +0,0 @@
1
- # Standards conformance audit
2
-
3
- What URLCode implements of each RFC and specification it touches or names,
4
- verified against the code and the test suite. The first pass was written at
5
- `817f0ab` (2026-09-17, Node v22.22.2); every row was re-verified against the
6
- source at `8d7dd01` (2026-09-18) and now cites the function or constant that
7
- implements the behavior rather than a line number, so a row stays checkable
8
- after the file around it moves. Every row also names the test that proves it;
9
- "untested" means no test in `test/` asserts that specific behavior. Where the
10
- code was not conclusive, the behavior was observed by running a server with
11
- the test helpers and sending raw requests over `node:net` / `node:http`; those
12
- rows say "probed". A row that could not be re-verified says
13
- "unverified at 8d7dd01" rather than guessing.
14
-
15
- Status vocabulary:
16
-
17
- - **conforms**: the implemented subset follows the standard's requirements.
18
- - **partial**: some of the standard is implemented and the rest is out of
19
- scope or approximated; the deviation is stated with its rationale.
20
- - **deviates**: the runtime does something the standard forbids or requires
21
- differently; listed again in the gap list at the end.
22
- - **not applicable**: the standard is only named, or is delegated to a
23
- platform, and no code claims to implement it.
24
-
25
- ## Summary
26
-
27
- | Standard | Status | Section |
28
- |---|---|---|
29
- | RFC 9110 HTTP Semantics | partial (408 on idle timeout is a SHOULD the server does not send) | [1](#1-rfc-9110-http-semantics) |
30
- | RFC 9111 HTTP Caching | partial | [2](#2-rfc-9111-http-caching) |
31
- | RFC 5861 stale-while-revalidate / stale-if-error | partial | [3](#3-rfc-5861-stale-while-revalidate-and-stale-if-error) |
32
- | RFC 8246 immutable | conforms | [4](#4-rfc-8246-immutable) |
33
- | RFC 9213 targeted cache control | conforms | [5](#5-rfc-9213-targeted-cache-control) |
34
- | RFC 6585 additional status codes (429, 431) | conforms | [6](#6-rfc-6585-additional-http-status-codes) |
35
- | RFC 9112 HTTP/1.1 message syntax | partial (asterisk-form 400) | [7](#7-rfc-9112-http11-message-syntax) |
36
- | RFC 3986 URI | partial (dot segments rejected, not normalized) | [8](#8-rfc-3986-uri-and-percent-encoding) |
37
- | RFC 7239 Forwarded | not applicable (only `X-Forwarded-For`) | [9](#9-rfc-7239-forwarded-and-x-forwarded-for) |
38
- | RFC 9309 robots.txt | conforms (generated by `site.robots`) | [10](#10-rfc-9309-robotstxt) |
39
- | RFC 6797 HSTS | conforms | [11](#11-rfc-6797-hsts) |
40
- | RFC 1950/1952 (deflate/gzip), RFC 7932 (br), RFC 8878 (zstd) | conforms | [12](#12-rfc-19501952-rfc-7932-rfc-8878-content-codings) |
41
- | RFC 6265 Set-Cookie | partial (emission only) | [13](#13-rfc-6265-set-cookie) |
42
- | RFC 6266 / 8187 Content-Disposition | conforms (library) | [14](#14-rfc-6266--rfc-8187-content-disposition) |
43
- | draft-ietf-httpapi-ratelimit-headers | conforms to the draft syntax (Internet-Draft, not an RFC) | [15](#15-ietf-httpapi-ratelimit-header-fields-internet-draft) |
44
- | web-bot-auth drafts | not applicable | [16](#16-web-bot-auth-drafts) |
45
- | OWASP Secure Headers Project | conforms to the pinned table | [17](#17-owasp-secure-headers-project) |
46
- | CSP Level 3 | partial (emits one obsolete directive from OSHP) | [18](#18-content-security-policy-level-3) |
47
- | JSON Schema 2020-12 | conforms (Ajv 8.20.0) | [19](#19-json-schema-2020-12) |
48
- | YAML 1.2 | conforms (JSON-compatible profile) | [20](#20-yaml-12) |
49
- | CIDR notation | conforms | [21](#21-cidr-notation-for---trusted-proxies) |
50
-
51
- ## 1. RFC 9110 HTTP Semantics
52
-
53
- | Requirement | Status | Behavior | Test |
54
- |---|---|---|---|
55
- | §9 methods; default GET/HEAD | conforms | `src/router.ts` `methodsDefault = ['GET','HEAD']`; an explicit `methods` list replaces it when `compileRoutes` builds the route; schema enum under `methods` in `schemas/urlcode.schema.json` | `test/http.test.ts` "literal precedence, methods, HEAD, query isolation, disabled/expired and health" |
56
- | §15.5.6 405 with `Allow` | conforms | `Runtime.handle` in `src/runtime.ts` answers `405` with `allow: <route.methods>` after host policies ran; health probes in `startServer` (`src/server.ts`) answer `405` with `allow: GET, HEAD`; the Worker does the same in `src/cloudflare.ts` | same test (`allow === 'GET, HEAD'`); `test/policies.test.ts` "interoperability: ... 405 carries policy headers"; `test/aws.test.ts` "a Lambda response carries exactly ..." (405 on POST) |
57
- | §9.1 unrecognized method → 501 | partial | Node's parser refuses unknown tokens before the runtime sees them; the `clientError` handler in `startServer` writes `400`. Probed: `BREW /r` → 400. Rationale: llhttp rejects at parse time; the runtime never sees the method | untested |
58
- | §9.3.2 HEAD suppresses body | conforms | `prepareResponse` in `src/http-response.ts` drops the body for HEAD and for 204/205/304 (`bodyless`) | `test/http.test.ts` (HEAD `/go` body `''`); `test/http-policy.test.ts`; `test/vercel.test.ts` |
59
- | §8.6 `Content-Length` on HEAD MUST equal what GET would send | conforms | `prepareResponse` measures `result.contentLength ?? result.body.length` on the result's body before the body is dropped for HEAD, so a `respond`, function or middleware result reports the GET length. Assets set `contentLength` in `assetResponse` (`src/assets.ts`) and cache hits in `served` (`src/policies/cache.ts`); compression leaves a dynamic body identity on HEAD (`onResponse` in `src/policies/compression.ts`), so the identity length is stated | `test/standards.test.ts` "HEAD states the length GET would send on declared and function responses (RFC 9110 §8.6)" |
60
- | §15.4 redirects: 301/302/303/307/308, absolute `Location` | conforms | schema enum under `redirect.status` in `schemas/urlcode.schema.json`; `redirectLocation` in `src/match.ts` builds `Location` from `new URL(...).href`; default 302 where `Runtime.handle` builds the redirect result (`route.redirect.status || 302`), and stored links carry their own status | `test/http.test.ts` (location assertions); `test/http-policy.test.ts` "YAML response headers apply to functions and redirects" |
61
- | §10.2.2 `Location` reserved from YAML | conforms | `reserved` in `src/http-policy.ts` includes `location`, `allow`, `content-range`, `accept-ranges`, `etag`, `last-modified`, `content-encoding` | `test/http-policy.test.ts` "invalid response policy and contradictory declarations fail activation" |
62
- | §7.6.1 hop-by-hop headers never set by a handler | conforms | `forbiddenHeaders` in `src/http-response.ts` (connection, keep-alive, transfer-encoding, content-length, upgrade, trailer, proxy-authenticate, proxy-authorization, te) stripped by `prepareResponse`, refused in YAML by `compileHttp` (`reserved` in `src/http-policy.ts`), and filtered from error answers by `errorResponse` | `test/http-policy.test.ts` "invalid response policy ..." |
63
- | §5.6.2 field names are tokens; §5.5 no CTLs in values | conforms | `token` and `invalidValue` in `src/header-validation.ts` (`validateHeaderName`, `validateHeaderValue`), applied by `prepareResponse`, `compileHttp` and `compile` in `src/policies/security.ts` | `test/header-validation.test.ts` all three tests (compared against `node:http` over 0x00–0x11F) |
64
- | §8.8.3 ETag, strong vs weak | conforms | Assets: strong `"sha256(type+disposition+body)"` computed in `compileAssets` (`read`). Precompressed variants: strong tag with the `SUFFIX` coding suffix inside the quotes (`etagFor` in `src/policies/compression.ts`). Dynamically compressed bodies: handler tag weakened to `W/...` (`etagFor` with `strong = false`). `revalidate` strategy: strong SHA-256 over the body when the handler sent none (`revalidate` in `src/policies/cache.ts`) | `test/assets.test.ts` "asset conditions, byte ranges ..."; `test/policy-compression.test.ts` "page assets are precompressed once, served by reference with a suffixed strong ETag, and revalidate" and "function JSON compresses with a weak ETag ..."; `test/policy-cache.test.ts` "revalidate answers 304 ..." |
65
- | §8.8.2 `Last-Modified` | conforms | second-resolution IMF-fixdate from mtime (`modified` in `compileAssets`) | `test/assets.test.ts` (304 on `if-modified-since`) |
66
- | §13.1.1 `If-Match` strong comparison | conforms | `matches(value, weak=false)` in `assetResponse` compares the full tag, so `W/"x"` never matches; `*` matches | `test/assets.test.ts` (`if-match: W/<etag>` → 412) |
67
- | §13.1.2 `If-None-Match` weak comparison | conforms | `matches(value, weak=true)` in `assetResponse` strips `W/`; `*` matches; `noneMatch` in `src/policies/cache.ts` and in `src/policies/compression.ts` do the same for their own tags | `test/assets.test.ts` (`W/<etag>` → 304); `test/policy-cache.test.ts` "revalidate answers 304 ..." |
68
- | §13.1.3 / §13.1.4 date conditions ignored when the ETag condition is present; invalid dates ignored | conforms | `assetResponse`: `If-Unmodified-Since` is read only when `If-Match` is absent, `If-Modified-Since` only when `If-None-Match` is absent; `Date.parse` → `NaN` makes both comparisons false, i.e. the header is ignored | `test/assets.test.ts` (`if-none-match: "other"` + matching `if-modified-since` → 200) |
69
- | §13.2.2 evaluation order If-Match → If-Unmodified-Since → If-None-Match → If-Modified-Since → If-Range | conforms | `assetResponse` in that order; Range evaluated after preconditions and only for GET | `test/assets.test.ts` (`range` + `if-none-match` → 304; HEAD + `range` → 200 with full length) |
70
- | §14 Range, single byte range; §14.4 206 `Content-Range`; §15.5.17 416 with `bytes */size` | conforms | `assetResponse`: suffix and open-ended ranges, `BigInt` arithmetic, 416 with `bytes */<size>`; multiple/malformed/non-`bytes` units ignored (§14.2 "MAY ignore") | `test/assets.test.ts` "asset conditions, byte ranges, empty files and HEAD obey HTTP ordering" |
71
- | §13.1.5 `If-Range` | partial | `assetResponse`: only an exact strong ETag match enables the range; a date `If-Range` is treated as non-matching and the full 200 is sent. Rationale (comment above the range check, `docs/ASSETS.md`): a date validator is only usable when it is known to be strong; sending the full representation is the prescribed fallback for a non-matching validator | `test/assets.test.ts` (`if-range: "old"` → 200; `if-range: <etag>` → 206) |
72
- | §15.4.5 304 header set | conforms | Assets keep `Content-Type`, `ETag`, `Last-Modified`, `Cache-Control`, `Accept-Ranges` (`headers` in `assetResponse`); `Content-Length` omitted for 304 (`bodyless` in `prepareResponse`). `revalidate` keeps the `kept` set in `src/policies/cache.ts` (`etag, cache-control, cdn-cache-control, vary, last-modified, content-location, expires, date, content-type`) | `test/policy-cache.test.ts` "revalidate answers 304 ..." (checks `etag`, `cache-control`, `content-type` on the 304) |
73
- | §12.5.3 `Accept-Encoding` negotiation | conforms | `negotiate` in `src/policies/compression.ts`: q-weights, `*` for unnamed codings, highest weight wins, project order breaks ties; identity is sent when nothing is acceptable, including `identity;q=0` / `*;q=0` (§12.5.3 permits either identity or 415; the code chooses identity, comment above `negotiate`) | `test/policy-compression.test.ts` "negotiation follows RFC 9110 q-values with the project order as tie-break" |
74
- | §8.4 `Content-Encoding` | conforms | set only by `onResponse` in `src/policies/compression.ts`; reserved from YAML (`reserved` in `src/http-policy.ts`); request bodies with a non-identity `Content-Encoding` are refused with 415 by `checkRequest` | `test/policy-compression.test.ts`; `test/http-policy.test.ts` "request body policies reject size, media, encoding and malformed JSON" |
75
- | §12.5.5 `Vary` | conforms | `withVary` in `src/policies/compression.ts` adds or merges `Vary: Accept-Encoding` and respects `*`, also on 304/206 (`varied` is returned for those); cache strategies merge declared `vary` names (`mergeVary` in `src/policies/cache.ts`) | `test/policy-compression.test.ts` "declared text responses compress ... Vary is set once"; `test/policy-cache.test.ts` "vary headers separate keys and are emitted ..." |
76
- | §15 status codes used | conforms | 400 (`parseTarget` in `src/match.ts`, `checkRequest` in `src/http-policy.ts`), 404 (`Runtime.handle`, `assetResponse`), 405 (`Runtime.handle`), 410 (`Runtime.handle`, expired routes and links), 412/304/206/416 (`assetResponse`), 413 (`readBody` in `src/server.ts`, `checkRequest`), 414 (`parseTarget`), 415 (`checkRequest`), 429/503 (throttle), 431 (`clientError` in `startServer`), 502/504 (function failures, see `docs/SPECIFICATION.md` "Functions"), 503 (`startServer` admission and shutdown). Rationale for 502/504 on sandbox failures: the function pool is treated as an upstream | `test/http.test.ts`, `test/http-policy.test.ts`, `test/policy-throttle.test.ts`, `test/standards.test.ts` |
77
- | §15.5.9 408 on idle timeout (SHOULD) | partial | `startServer`: `server.setTimeout(15000, socket => socket.destroy())` closes without a status, and the `clientError` handler answers `ERR_HTTP_REQUEST_TIMEOUT` with `400` rather than `408`; probed: an incomplete request head is dropped after ~15 s with no response bytes. Rationale: the socket is destroyed to free the slot; the requirement is a SHOULD | untested |
78
- | §10.1.5 `User-Agent` | conforms | read-only matching in `onRequest` (`src/policies/agents.ts`); the header value is never logged (the list label is) | `test/policy-agents.test.ts` "a bundled deny list refuses matching agents and logs the list name, never the header" |
79
- | §10.2.3 `Retry-After` (delay-seconds) | conforms | `onRequest` in `src/policies/throttle.ts` writes `retry-after: <reset>` with integer seconds from `observe` | `test/policy-throttle.test.ts` "quota reached answers 429 with Retry-After and RateLimit headers" |
80
- | §6.4.1 `Content-Length` on every non-bodyless response | conforms | `prepareResponse`; error answers in `errorResponse` | `test/aws.test.ts` (header comparison includes `content-length`); `test/policy-compression.test.ts` (`content-length` equals encoded length) |
81
-
82
- ## 2. RFC 9111 HTTP Caching
83
-
84
- | Requirement | Status | Behavior | Test |
85
- |---|---|---|---|
86
- | §5.2.2 response directives `no-store`, `no-cache`, `public`, `private`, `max-age` | conforms | the `switch (strategy)` in `compile` (`src/policies/cache.ts`): `no-store`→`no-store`, `revalidate`→`no-cache`, `public`→`public, max-age=N`, `private`→`private, max-age=N`; runtime default `cache-control: no-store` when a handler sets none (`prepareResponse`), errors always `no-store` (`errorResponse`); throttle and agents denials `no-store` (`onRequest` in each policy) | `test/policy-cache.test.ts` "each strategy emits its catalogue headers; explicit fields override what the strategy implies" |
87
- | §5.1 `Age` on a stored response | conforms | `served` in `src/policies/cache.ts` replaces any `Age` with `floor((now-storedAt)/1000)` | `test/policy-cache.test.ts` "swr serves fresh from the origin cache ..." |
88
- | §3 storing: never store `Set-Cookie`, `private`, `no-store`, secret-bearing or oversized results | conforms | `storable` in `onResponse` (`src/policies/cache.ts`); only statuses in `statuses` (`DEFAULT_STATUSES = [200,301,302,404,410]`) | `test/policy-cache.test.ts` "Set-Cookie, secret-bearing routes, handler no-store and oversized bodies are never stored" |
89
- | §4.1 `Vary` in the cache key | conforms | `keyFor` = route, path, query, declared `vary` header values; `mergeVary` emits `Vary` on storable statuses in `onResponse` | `test/policy-cache.test.ts` "vary headers separate keys and are emitted ..." |
90
- | §4.3 validation requests never served from the origin cache | conforms | `conditional` list in `src/policies/cache.ts`; `onRequest` bypasses for `If-None-Match`, `If-Modified-Since`, `If-Match`, `If-Unmodified-Since`, `Range` | `test/policies.test.ts` "interoperability: conditional requests bypass origin hits ..." |
91
- | §4.4 invalidation on unsafe methods | partial | only GET results are stored (`onRequest` opens a flight for GET only) and only GET/HEAD looked up; a POST to the same route does not invalidate an entry. Rationale: the store belongs to one runtime and is dropped on reload (`close` in `src/policies/cache.ts`); handlers are side-effect free by design | untested |
92
- | §5.2.1 request directives (`no-cache`, `max-age=0`, `no-store`) honored by a cache | **partial** | the origin memory cache ignores request `Cache-Control` and `Pragma` (`onRequest` reads only the `conditional` headers). Probed at `817f0ab`: `GET /pub` with `Cache-Control: no-cache` and with `max-age=0` both served from memory with `age: 0`; the code path is unchanged at `8d7dd01`. Rationale (module comment at the top of `src/policies/cache.ts`, `docs/policies/cache.md` "Origin memory cache"): this is the origin's own micro-cache in the NGINX `proxy_cache` sense, part of the origin server rather than a shared cache in the RFC 9111 sense; a client cannot bypass it, as it cannot bypass an origin's internal memoization | untested |
93
- | §4.2.4 serving stale | partial | stale served only within `stale-while-revalidate` and only once per entry (`entry.revalidating` in `onRequest`), never beyond; see §3 below | `test/policy-cache.test.ts` "swr serves fresh ... serves stale once and refreshes on the next request" |
94
- | `micro` strategy: `no-store` to clients, 1–5 s origin memory | conforms (origin-side) | `case 'micro'` in `compile` (`MICRO_MAX = 5`); the client-facing header is honest (`no-store`) while the origin memoizes | `test/policy-cache.test.ts` "micro caches at the origin for one second while telling clients no-store" |
95
- | Explicit YAML / handler `Cache-Control` beats the strategy | conforms | `yamlCacheControl` and `inheritedAsset` in `compile`; `owned` in `onResponse` | `test/policy-cache.test.ts` "vary headers ... explicit YAML cache-control wins over the strategy" and "assets keep their handler cacheControl under an inherited policy ..." |
96
-
97
- ## 3. RFC 5861 stale-while-revalidate and stale-if-error
98
-
99
- | Requirement | Status | Behavior | Test |
100
- |---|---|---|---|
101
- | §3 `stale-while-revalidate=N` emitted | conforms | `case 'swr'` and `case 'sie'` in `compile` (`src/policies/cache.ts`) | `test/policy-cache.test.ts` "each strategy emits its catalogue headers" |
102
- | §3 background revalidation at the origin cache | partial | a policy has no handle to the handler, so a stale hit is served once and flagged (`entry.revalidating` in `onRequest`); the next request refreshes synchronously; a failed refresh clears the flag (`onError`). Documented in `docs/policies/cache.md` "`swr` at the origin" | "swr serves fresh from the origin cache, serves stale once and refreshes on the next request"; "a failed fill releases waiters to the handler and never stores" |
103
- | §4 `stale-if-error=N` emitted | conforms | `case 'sie'` in `compile` | "each strategy emits its catalogue headers" |
104
- | §4 serving stale on error at the origin | partial | header-only: `onError` only releases waiters and clears the flag; it never returns a fallback result although `Runtime.handle` would accept one (the `policy.error` loop in its catch block). Documented in `docs/policies/cache.md` "`sie` limitation" | untested (no test asserts a 5xx is *not* replaced) |
105
-
106
- ## 4. RFC 8246 immutable
107
-
108
- | Requirement | Status | Behavior | Test |
109
- |---|---|---|---|
110
- | `immutable` extension emitted with a long `max-age` | conforms | `case 'immutable'` in `compile`: `public, max-age=<maxAge ?? YEAR>, immutable`; refused unless the pattern has a segment matching `hashedSegment` (8+ hex chars) or `hashedParameter` (a digest-named parameter), or `force: true` | `test/policy-cache.test.ts` "immutable is refused on unhashed paths and accepted with a hashed segment, a hash parameter or force" |
111
- | Asset `cacheControl: public, max-age=31536000, immutable` | conforms | allowed literal in the schema; passed through as `asset.cache` (`compileAssets`) and emitted by `assetResponse` | `test/assets.test.ts` "native page, download and static handlers ..." (cacheControl variants) |
112
-
113
- ## 5. RFC 9213 targeted cache control
114
-
115
- | Requirement | Status | Behavior | Test |
116
- |---|---|---|---|
117
- | `CDN-Cache-Control` as a Structured-Field dictionary | conforms | `cdn-only` emits `Cache-Control: no-store` plus `CDN-Cache-Control: max-age=N` (`case 'cdn-only'` in `compile`, written by `onResponse` when `owned`); not in the `reserved` set of `src/http-policy.ts`, so a YAML `CDN-Cache-Control` is replaced by the strategy unless YAML also sets `cache-control` | `test/policy-cache.test.ts` "each strategy emits its catalogue headers" |
118
-
119
- ## 6. RFC 6585 additional HTTP status codes
120
-
121
- | Requirement | Status | Behavior | Test |
122
- |---|---|---|---|
123
- | §4 429 Too Many Requests with an explanatory body | conforms | `bodies` and `onRequest` in `src/policies/throttle.ts`: `429`, `Too many requests\n`, `Retry-After`, `RateLimit*`; any 4xx/5xx `status` allowed with a matching short body | `test/policy-throttle.test.ts` "quota reached answers 429 ..." and "status other than 429 gets a matching short body" |
124
- | §5 431 Request Header Fields Too Large | conforms | `startServer` sets `maxHeaderSize: 16384`; the `clientError` handler answers `HPE_HEADER_OVERFLOW` with `431 Request Header Fields Too Large` and every other parse failure with `400 Bad Request` | `test/standards.test.ts` "oversized header fields answer 431, not 400 (RFC 6585 §5)" |
125
-
126
- ## 7. RFC 9112 HTTP/1.1 message syntax
127
-
128
- | Requirement | Status | Behavior | Test |
129
- |---|---|---|---|
130
- | §3.2.1 origin-form request target | conforms | `parseTarget` in `src/match.ts` requires a leading `/`, rejects `//`, `#`, controls, `\`; length > 8192 is 414 | `test/http.test.ts` "malformed and ambiguous URL encodings fail before routing" (`//evil.example`, `/%00` ...) |
131
- | §3.2.2 absolute-form MUST be accepted by servers | conforms | `originForm` in `src/server.ts` strips the scheme and authority textually before `parseTarget`, so `GET http://example.test/go?x=1` routes as `/go?x=1` and a bare authority becomes `/`. The Worker and Lambda paths reconstruct an origin-form target themselves | `test/standards.test.ts` "request targets: 414 for over-long, absolute-form accepted, asterisk-form refused (RFC 9112 §3)" |
132
- | §3.2.4 asterisk-form for `OPTIONS *` | deviates (minor) | `parseTarget` refuses `*` (no leading `/`) → 400. No route can be `*`, so no handler could answer it anyway | same test (`OPTIONS *` → 400) |
133
- | §3 414 for over-long request targets | conforms | `parseTarget` throws `HttpError(414, 'URI too long')` for targets over 8192 bytes. Node's own limit (`maxHeaderSize`) answers 431 earlier for the whole head | same test (9000-byte path → 414) |
134
- | §3.2 missing `Host` → 400 | conforms | enforced by Node's parser; probed `GET /r HTTP/1.1` with no `Host` → 400 | untested |
135
- | §6.1 `Content-Length` + `Transfer-Encoding` both present → reject | conforms | Node parser; probed → 400 | untested |
136
- | §7 chunked request bodies | conforms | Node decodes; `readBody` in `src/server.ts` counts decoded bytes against the limit and answers 413 | `test/http.test.ts` "chunked oversized body returns 413 and server remains usable" |
137
- | header field limits | conforms | `maxHeaderSize: 16384`, `headersTimeout: 10000`, `requestTimeout: 15000`, `keepAliveTimeout: 5000` in `http.createServer` options, `maxRequestsPerSocket = 1000`, `maxConnections = 1024` (all in `startServer`); response headers ≤ 16 KiB / 256 pairs (`compileHttp` and `decorateResponse` in `src/http-policy.ts`) | `test/http.test.ts` "limits reject oversized requests and responses" |
138
- | §9.3 `Connection: close` on error answers | conforms | `writeError` in `src/http-response.ts`; the `clientError` reply in `startServer` | untested |
139
- | duplicate header fields | conforms | raw headers walked in `startServer` (`headerCounts`), counts passed to policy; duplicated scalar header parameters → 400 (`contextFor` in `src/match.ts`), duplicated `Content-Type` → 400 (`checkRequest`), duplicate `X-Forwarded-For` ignored (`startServer` passes it only when the count is exactly 1), duplicate `X-Request-Id` not trusted (`startServer`, `safeRequestId`) | `test/http.test.ts` "header inputs are case insensitive and duplicate scalars fail" |
140
- | HTTP/1.0 requests | conforms | Node answers with `Connection: close`; probed | untested |
141
-
142
- ## 8. RFC 3986 URI and percent-encoding
143
-
144
- | Requirement | Status | Behavior | Test |
145
- |---|---|---|---|
146
- | §2.1 `%HH` must be two hex digits, case-insensitive | conforms | `parseTarget`: `/%(?![0-9a-f]{2})/i` → 400; probed `/r%41` → decoded to `/rA` (404, i.e. accepted and decoded) | `test/http.test.ts` "malformed and ambiguous URL encodings fail before routing" (`/%ZZ`, `/ok?q=%`) |
147
- | §2.2 encoded delimiters keep their encoding (`%2F` is not `/`) | conforms (stricter) | `%2F` and `%5C` in the path are refused with 400 rather than kept encoded (`parseTarget`), so an encoded slash can never span a segment. Rationale: `docs/ROUTING.md` "Encoded slashes ... are rejected" | same test (`/%2f`, `/%5c`) |
148
- | decode exactly once | conforms | `decodeURIComponent` once on path and once on query in `parseTarget`; `%252F` stays `%2F` after one decode and is re-encoded into the redirect by `redirectLocation` (`encodeURIComponent`) | same test (`/a%252Fb` → `.../a%252Fb`) |
149
- | non-UTF-8 percent sequences | stricter than RFC 3986 | `/%ff` → 400 because `decodeURIComponent` requires UTF-8 (RFC 3987/WHATWG behavior); RFC 3986 itself permits arbitrary octets | same test (`/%ff`, `/ok?q=%ff`) |
150
- | §5.2.4 dot segments | deviates (deliberate) | `.` and `..` segments are refused with 400 (`parseTarget`) rather than removed by `remove_dot_segments`; `%2e` decodes to `.` and is refused the same way. Rationale: `docs/SPECIFICATION.md` "Routes" — no normalization so a route key means exactly one path | same test (`/..`, `/%2e`); `test/assets.test.ts` (`/assets/../urlcode.yaml`, `/assets/%2e%2e/urlcode.yaml` → 400) |
151
- | §3.3 empty path segments | conforms | `//` at the start refused (`parseTarget`, network-path ambiguity); internal empty segments only match a literal route containing them; `{param}` requires a non-empty segment (`matchRoute`) | `test/http.test.ts` (`//evil.example`) |
152
- | §3.4 query | conforms | split on the first `?` (`parseTarget`); parsed by `URLSearchParams`, which applies `application/x-www-form-urlencoded` rules (`+` = space), a WHATWG rule RFC 3986 does not define; the validity check in `parseTarget` mirrors it | "typed inputs, defaults, arrays, mapping and passthrough" |
153
- | §3.5 fragment never in a request target | conforms | `#` refused (`parseTarget`) | untested directly |
154
- | non-ASCII octets in the target | conforms | refused by Node's parser (probed raw `GET /caf\xe9` → 400) | untested |
155
- | route keys | conforms | `segments` in `src/router.ts` forbids `?#%\` and whitespace/controls in route patterns; ≤ 2048 chars, ≤ 32 segments | `test/config.test.ts` "semantic validation rejects ambiguous routes and unsafe redirects" |
156
- | redirect URL validation | conforms | absolute `http(s)`, no userinfo, placeholders only in path segments (`compileRoutes` redirect block in `src/router.ts`); single-component `encodeURIComponent` and the 16 KiB `Location` cap in `redirectLocation` | `test/http.test.ts` (`/p/hello%20world` → `.../hello%20world`); `test/config.test.ts` |
157
-
158
- ## 9. RFC 7239 Forwarded and X-Forwarded-For
159
-
160
- | Requirement | Status | Behavior | Test |
161
- |---|---|---|---|
162
- | RFC 7239 `Forwarded` field | **not applicable** (not implemented) | no code reads `Forwarded`; only `x-forwarded-for` is read (`startServer` in `src/server.ts`, the request adapter in `src/vercel.ts`). `docs/SPIKE-EXTENSIONS.md` now states "RFC 7239 `Forwarded` is not parsed" | none |
163
- | `X-Forwarded-For` (de-facto, no RFC) trusted only from configured proxies | conforms to common practice | `resolveClient` in `src/client-address.ts`: peer must be in `--trusted-proxies`; walk from the right skipping trusted hops; all-trusted chain → leftmost; header ignored when absent or duplicated (`startServer` requires exactly one field) | `test/policies.test.ts` "client identity trusts forwarded headers only from configured proxies" |
164
- | malformed entries | conforms, documented | entries that are not a bare IP are skipped and the walk continues (`resolveClient` filters `normalizeAddress` misses); `normalizeAddress` strips a trailing `:port` from IPv4 entries and the brackets from `[v6]:port`, so RFC 7239-style `203.0.113.5:1234` resolves to `203.0.113.5`. The comment above `resolveClient` and `docs/POLICIES.md` now say "a malformed entry is skipped" | `test/standards.test.ts` "forwarded entries with ports and embedded IPv4 in IPv6 ranges resolve correctly"; `test/policies.test.ts` (`'garbage, 198.51.100.1'` → `198.51.100.1`) |
165
- | bracketed IPv6 and IPv4-mapped peers | conforms | `normalizeAddress` strips `[...]` and `::ffff:` | same test (`::ffff:10.1.2.3` peer) |
166
- | HSTS / origin never derived from `X-Forwarded-Proto`/`Host` | conforms | `onResponse` in `src/policies/security.ts` reads `request.origin`, which is the operator origin (`publicOrigin` in `startServer`) | `test/policy-security.test.ts` "HSTS follows the request origin scheme" (`x-forwarded-proto: https` does not enable it) |
167
-
168
- ## 10. RFC 9309 robots.txt
169
-
170
- | Requirement | Status | Behavior | Test |
171
- |---|---|---|---|
172
- | Serving `robots.txt` | conforms | `robotsRoute` in `src/site.ts` generates `/robots.txt` from `site.robots` (bundled agent lists or paths; `User-agent:` / `Disallow:` / `Allow:` groups, an optional `Sitemap:` line when the public origin is known); a hand-written `/robots.txt` route takes precedence. The runtime never reads a `robots.txt` itself; the `agents` policy enforces the same lists for clients that ignore it (`docs/policies/agents.md` "robots.txt") | `test/site.test.ts` "every site key generates a native route with the expected body, type and cache policy" and "robots lists resolve through the bundled agent lists and skip non-token names" |
173
-
174
- ## 11. RFC 6797 HSTS
175
-
176
- | Requirement | Status | Behavior | Test |
177
- |---|---|---|---|
178
- | §6.1 directive syntax `max-age=...; includeSubDomains` | conforms | `oshp` table in `src/policies/security.ts`: `max-age=31536000; includeSubDomains` | `test/policy-security.test.ts` "HSTS follows the request origin scheme" |
179
- | §7.2 MUST NOT send STS over non-secure transport | conforms | `onResponse` in `src/policies/security.ts` skips `strict-transport-security` unless `request.origin` starts with `https:`; never inferred from forwarded headers | same test (plain origin → absent; `x-forwarded-proto` ignored; `origin: https://...` → present) |
180
- | Worker | conforms | scheme from the request URL (`docs/policies/security.md` "Targets") | `test/policy-security.test.ts` "the Worker emits the same security headers as the self-hosted server" |
181
-
182
- ## 12. RFC 1950/1952, RFC 7932, RFC 8878 content codings
183
-
184
- | Coding | Status | Behavior | Test |
185
- |---|---|---|---|
186
- | `gzip` (RFC 1952) | conforms | `codecs.gzip` in `src/policies/compression.ts` (`zlib.gzipSync`); dynamic level 6, stored 9 | `test/policy-compression.test.ts` "declared text responses compress per Accept-Encoding ..." (round-trips with `gunzipSync`) |
187
- | `deflate` = zlib container (RFC 1950 over RFC 1951), as RFC 9110 §8.4.1.1 defines the coding | conforms | `codecs.deflate` (`zlib.deflateSync`) produces the zlib format, not raw deflate | untested end to end (only `decode.deflate` is defined in the test) |
188
- | `br` (RFC 7932) | conforms | `codecs.br` (`zlib.brotliCompressSync` with `BROTLI_PARAM_QUALITY` and `BROTLI_PARAM_SIZE_HINT`); dynamic quality 4, stored 9 | same test (`brotliDecompressSync` round-trip) |
189
- | `zstd` (RFC 8878) | conforms when available | `codecs.zstd` (`zlib.zstdCompressSync`, dynamic 3, stored 12); `zstdAvailable` refuses it at activation (`compile`) when Node lacks it; available on the audited Node (probed) | "zstd is honoured only when node:zlib provides it; serverless targets delegate the policy" |
190
- | request-body decompression | not implemented (documented) | `checkRequest` in `src/http-policy.ts` → 415 for any non-identity `Content-Encoding`; `docs/HTTP.md` "no automatic decompression" | `test/http-policy.test.ts` "request body policies reject size, media, encoding ..." |
191
- | BREACH mitigation (not an RFC; CVE-2013-3587) | conforms to the documented rule | `onResponse` in `src/policies/compression.ts` skips when the route holds secrets or the response sets a cookie unless `allowWithSecrets` | "function JSON compresses with a weak ETag; Set-Cookie and secrets skip unless allowWithSecrets" |
192
-
193
- ## 13. RFC 6265 Set-Cookie
194
-
195
- | Requirement | Status | Behavior | Test |
196
- |---|---|---|---|
197
- | §3 one `Set-Cookie` header per cookie, never folded | conforms | `prepareResponse` collects cookies separately (`cookies`) and `writeResponse` writes them as an array; YAML `Set-Cookie` is the only list-valued header (`compileHttp`); AWS format 2.0 `cookies` array (`src/aws.ts`) | `test/http.test.ts` "function Request/Response ABI, scoped bindings, cookies ..."; `test/aws.test.ts` "cookies arrive through the format 2.0 array and leave through it" |
198
- | §4.1.1 cookie syntax validation | partial | only the generic header rules apply (token name, no CTLs); no attribute parsing, signing or `Cookie` request parsing (`docs/HTTP.md` "Still outside this contract") | n/a |
199
- | cookies vs caching/compression | conforms | responses with `Set-Cookie` are never stored (`storable` in `onResponse`, `src/policies/cache.ts`) and not compressed by default (`onResponse` in `src/policies/compression.ts`) | `test/policy-cache.test.ts` "Set-Cookie, secret-bearing routes ..."; `test/policy-compression.test.ts` |
200
-
201
- ## 14. RFC 6266 / RFC 8187 Content-Disposition
202
-
203
- | Requirement | Status | Behavior | Test |
204
- |---|---|---|---|
205
- | `attachment; filename=...; filename*=UTF-8''...` | conforms | `content-disposition@3.0.0` `create()` (imported as `disposition`) in `compileAssets`; filenames with separators or controls refused just before it | `test/assets.test.ts` "native page, download and static handlers ..." (`filename*=UTF-8''r%C3%A9sum%C3%A9.bin`) and "assets reject unsafe paths ... " (`bad\r\nname`) |
206
-
207
- ## 15. IETF httpapi RateLimit header fields (Internet-Draft)
208
-
209
- The vocabulary is **draft-ietf-httpapi-ratelimit-headers**, an IETF
210
- Internet-Draft of the HTTPAPI working group. It is not an RFC; its field
211
- names and syntax have changed between revisions and may change again.
212
-
213
- | Requirement | Status | Behavior | Test |
214
- |---|---|---|---|
215
- | `RateLimit-Policy` as a Structured-Field list of items with `q` and `w` parameters | conforms to the current draft syntax | `headersFor` in `src/policies/throttle.ts`: `"default";q=<quota>;w=<window>` | `test/policy-throttle.test.ts` "quota reached answers 429 with Retry-After and RateLimit headers" (`'"default";q=2;w=60'`) |
216
- | `RateLimit` item with `r` (remaining) and `t` (seconds to reset) | conforms | `headersFor`: `"default";r=<remaining>;t=<reset>`; `t` from the sliding-window end (`observe`) | same test (`/^"default";r=1;t=\d+$/`) |
217
- | present on allowed and refused responses | conforms | `onRequest` attaches the budget to `state.pending`, `onResponse` writes it; refusals carry it directly (`withHeaders` in `onRequest`) | same test; `test/policies.test.ts` "... 405 carries policy headers" |
218
- | `pk` (partition key) parameter | not emitted | keys are internal (`keyFor`) | n/a |
219
- | refused requests not counted | conforms to the draft's guidance | `onRequest`: `entry.current++` only when not exceeded (or in `report` mode) | "window slides: the previous window fades out instead of resetting at once" |
220
-
221
- ## 16. web-bot-auth drafts
222
-
223
- | Requirement | Status | Behavior | Test |
224
- |---|---|---|---|
225
- | HTTP Message Signatures for bots (IETF `web-bot-auth` drafts) | **not applicable** | named in `docs/policies/agents.md` ("Matching is string matching") as host-plugin territory; no code | none |
226
-
227
- ## 17. OWASP Secure Headers Project
228
-
229
- | Requirement | Status | Behavior | Test |
230
- |---|---|---|---|
231
- | OSHP "best practices" values | conforms to the pinned table | `oshp` table in `src/policies/security.ts`, pinned as `oshpRevision = 'OSHP 2024 best practices'`; docs table generated from the same exported `profiles`; `X-Content-Type-Options: nosniff` supplied by the runtime on every response (`prepareResponse`, `errorResponse`) rather than the profile. The upstream JSON could not be fetched from the audit environment (proxy 403), so the table is verified against the code and docs, not re-verified against owasp.org | `test/policy-security.test.ts` "profile tables are frozen, ordered and consistent with each other", "profile headers land on redirects, declared responses and function results; existing headers win" |
232
- | fill-gaps semantics; `set`/`unset` | conforms | `compile` (`unset`, `set`, `overrideKeys`) and `onResponse` in `src/policies/security.ts` | "set overrides the profile and existing headers; unset drops a profile header" |
233
- | applied to errors and early denials | conforms | `Runtime.errorHeaders` and `finishPolicies` in `src/runtime.ts`; `errorResponse` merges the resolved policy headers after its fixed set | `test/policies.test.ts` "error responses carry the security headers of the matched route or the project"; `test/policy-security.test.ts` "early results that skip the handler still carry the profile" |
234
- | `X-Frame-Options` (RFC 7034, informational) `deny` | conforms | `oshp` table entry `x-frame-options: deny` | as above |
235
-
236
- ## 18. Content Security Policy Level 3
237
-
238
- | Requirement | Status | Behavior | Test |
239
- |---|---|---|---|
240
- | CSP3 serialized policy syntax | conforms | `default-src 'self'; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content` (`oshp` table, `content-security-policy`) | `test/policy-security.test.ts` (value equality with the frozen table) |
241
- | `block-all-mixed-content` | partial | this directive is obsolete in CSP3 / the current Mixed Content specification and is ignored by current browsers; it is emitted because OSHP recommends it. Harmless, but not a Level 3 directive | n/a |
242
- | `upgrade-insecure-requests` | conforms | defined by the W3C Upgrade Insecure Requests specification, delivered through CSP; emitted regardless of scheme (browsers ignore it on plain HTTP) | n/a |
243
- | Report-only and `report-to` | conforms (operator-supplied) | any `Content-Security-Policy-Report-Only` value is passed verbatim through `set` (`docs/policies/security.md`); the runtime does not host a reporting endpoint | "set overrides the profile ..." |
244
-
245
- ## 19. JSON Schema 2020-12
246
-
247
- | Requirement | Status | Behavior | Test |
248
- |---|---|---|---|
249
- | project schema dialect | conforms | `schemas/urlcode.schema.json` `$schema: https://json-schema.org/draft/2020-12/schema`; compiled by `ajv/dist/2020.js` (Ajv 8.20.0, `package.json` dependencies) as `validate` in `src/config.ts` with `strict: true`, `strictRequired: false`, `allowUnionTypes: true`, `allErrors: false` | `test/config.test.ts` "strict schema rejects unknown features and multiple handlers"; `test/policies.test.ts` "policies validate in YAML and unknown keys fail" |
250
- | input parameter schemas (subset: `type`, `enum`, `default`, `minLength`/`maxLength`, `minimum`/`maximum`, `items`, `maxItems`) | conforms | `inputValidator` in `src/router.ts` compiles each distinct schema with Ajv 2020 (`strict: false`, `allErrors: false` in `compileRoutes`); `default` is stripped before compiling because 2020-12 treats it as an annotation; semantic cross-checks in the `parameters` loop of `compileRoutes`; unsupported keywords (`pattern`, `format`, nested objects) are refused by the project schema, not silently ignored | `test/http.test.ts` "typed inputs, defaults, arrays, mapping and passthrough" |
251
- | standalone validators for the Worker | conforms | precompiled by the build into ES modules (`docs/CLOUDFLARE.md` "validators.js") | `test/cloudflare.test.ts` "the generated Worker entry and validators carry no imports the platform cannot resolve" |
252
- | bounded compilation | conforms | ≤ 1024 distinct schemas per snapshot (`inputValidator`) | untested |
253
-
254
- ## 20. YAML 1.2
255
-
256
- | Requirement | Status | Behavior | Test |
257
- |---|---|---|---|
258
- | YAML 1.2 core schema, JSON-compatible profile | conforms | `yaml@2.9.1` (`package.json` dependencies); `parseYaml` in `src/config.ts` calls `parseDocument(text, { version: '1.2', uniqueKeys: false, strict: true })`; parser errors and warnings fail | `test/config.test.ts` "YAML rejects ambiguity and nonportable constructs" |
259
- | no aliases, anchors, tags, merge keys, multiple documents | conforms (deliberate subset) | `parseYaml`: the `visit` callback refuses aliases, anchors and tags; `inspect` refuses `<<` and prototype keys; multi-document rejected by the parser in strict mode; `toJS({ maxAliasCount: 0 })` | same test (`&x`/`*x`, `!custom`, `!!str`, `---`, `<<`) |
260
- | string keys only, unique keys | conforms | the linear duplicate check over `node.items` in `parseYaml` (comment: "Our string-only profile permits equivalent linear duplicate detection") | same test (`x: 1\nx: 2`) |
261
- | scalars: string, finite number, boolean, null | conforms | `parseYaml`: the scalar check in `visit` and the `Number.isFinite` check in `inspect` (`.inf`/`.nan` refused) | same test (`.inf`, `.NaN`) |
262
- | YAML 1.2 boolean semantics (`yes`/`no` are strings) | conforms | follows from `version: '1.2'` | untested |
263
- | resource bounds | conforms | `MAX_CONFIG_BYTES` (32 MiB per file), `MAX_PROJECT_CONFIG_BYTES` (64 MiB aggregate, enforced by `readConfig`), 40 nesting levels (`inspect`), and `loadDocument` runs the parse in a worker with `resourceLimits` and a `timeoutMs` deadline (`src/config.ts`) | "configuration worker deadline terminates loading and releases admission"; "configuration worker enforces aggregate source budget across includes" |
264
-
265
- ## 21. CIDR notation for `--trusted-proxies`
266
-
267
- | Requirement | Status | Behavior | Test |
268
- |---|---|---|---|
269
- | `address[/prefix]` for IPv4 and IPv6 (RFC 4632 / RFC 4291 §2.3 notation) | conforms | `parseCidr` in `src/client-address.ts`; prefix defaults to the full length; prefix range checked; `compileTrustedProxies` accepts a comma-separated string or array, ≤ 256 ranges | `test/policies.test.ts` "client identity trusts forwarded headers only from configured proxies" (`10.0.0.0/8, ::1, 192.168.1.5`; `/33` refused) |
270
- | host bits set beyond the prefix | lenient | `10.1.2.3/8` accepted and masked at match time (`within`); probed | untested |
271
- | IPv4-mapped IPv6 (`::ffff:a.b.c.d`) | conforms | `toBytes` and `normalizeAddress` special-case `::ffff:` | same test (`::ffff:10.1.2.3`) |
272
- | other IPv4-embedded IPv6 forms (`64:ff9b::1.2.3.4`, RFC 4291 §2.2 form 3) | conforms | `toBytes` converts a trailing dotted quad into two 16-bit groups before splitting on `:` | `test/standards.test.ts` "forwarded entries with ports and embedded IPv4 in IPv6 ranges resolve correctly" (`parseCidr('64:ff9b::1.2.3.4/96')` bytes) |
273
- | zone identifiers (`fe80::1%eth0`) | accidental | `net.isIP` accepts them (probed → 6); `parseInt('1%eth0', 16)` in `toBytes` yields `1`, so the zone is silently ignored | untested |
274
-
275
- ## Gaps, ranked
276
-
277
- Closed since the first pass (rows above flipped to conforms): HEAD
278
- `Content-Length` on `respond`, function and middleware results (RFC 9110
279
- §8.6); 431 for oversized header fields (RFC 6585 §5); absolute-form request
280
- targets (RFC 9112 §3.2.2); 414 for over-long targets (RFC 9112 §3);
281
- `X-Forwarded-For` entries with a port and the "malformed entry" wording in the
282
- docs; IPv4-embedded IPv6 CIDRs other than `::ffff:`.
283
-
284
- 1. **408 collapsed into 400** (RFC 9110 §15.5.9; §1). Fix: in the `clientError` handler of `startServer` (`src/server.ts`) branch on `error.code === 'ERR_HTTP_REQUEST_TIMEOUT'` → `408`, keeping `Connection: close` and `Content-Length: 0`. Test: a raw-socket request head left incomplete past `headersTimeout` in `test/standards.test.ts`.
285
- 2. **asterisk-form request target answered 400** (RFC 9112 §3.2.4; §7). Fix: in `originForm` or `startServer` answer `OPTIONS *` with 405 + `Allow`, or leave as 400 and state it. Test: `test/standards.test.ts` already asserts the current 400; flip it with the fix.
286
- 3. **origin memory cache ignores request `Cache-Control: no-cache` / `max-age=0`** (RFC 9111 §5.2.1; §2). Either document it explicitly in `docs/policies/cache.md` "Origin memory cache" ("request cache directives are not honored: the store is part of the origin"), or add `cache-control` to the `conditional` bypass list in `src/policies/cache.ts` when it contains `no-cache` or `no-store`. Test: add a request with `cache-control: no-cache` to `test/policy-cache.test.ts` "swr serves fresh from the origin cache ..." asserting the chosen behavior.
287
- 4. **`stale-if-error` is header-only** (RFC 5861 §4; §3): documented; `onError` in `src/policies/cache.ts` could return `served(entry, age)` when an entry exists within `staleIfError` — `Runtime.handle` already accepts a fallback from an error hook. Test: a function that throws after a first successful fill.
288
- 5. **Untested conformance points** that hold today but have no guard: 304 omits `Content-Length` (assets and `revalidate`); `deflate` end-to-end round-trip; missing `Host` → 400; CL+TE → 400; `Connection: close` on error answers; YAML 1.2 `yes`/`no` as strings; ≤ 1024 input schemas. Each is one assertion in the test named in its row.
289
- 6. **`block-all-mixed-content`** in the OSHP CSP (§18): obsolete but harmless; keep while the pinned OSHP revision recommends it, and note it in `docs/policies/security.md` next to the CSP paragraph.
290
-
291
- ## Factual errors in existing documentation
292
-
293
- All four errors found in the first pass are corrected at `8d7dd01`:
294
-
295
- - `docs/policies/agents.md` "robots.txt" no longer carries a `respond` recipe with `headers:`; it points at `site.robots`, which generates the file.
296
- - `docs/policies/compression.md` "ETag and HEAD": "reports the identity length" is now true (`prepareResponse` measures the body before dropping it).
297
- - `docs/POLICIES.md` and the comment above `resolveClient` say "a malformed entry is skipped".
298
- - `docs/SPIKE-EXTENSIONS.md` says "RFC 7239 `Forwarded` is not parsed".
299
-
300
- ## How to keep this current
301
-
302
- - Any change to `src/http-response.ts` (what a response *is*) must extend `test/http-policy.test.ts` "declarative text, JSON and empty responses ..." and rerun the parity tests (`test/aws.test.ts` "a Lambda response carries exactly ...", `test/cloudflare.test.ts` "the Worker runtime answers exactly ..."), then update §1 here.
303
- - Any change to `parseTarget` in `src/match.ts`, `originForm` in `src/server.ts` or `segments` in `src/router.ts` must extend `test/http.test.ts` "malformed and ambiguous URL encodings fail before routing" or `test/standards.test.ts`, and update §7–§8.
304
- - Any change to `assetResponse` in `src/assets.ts` (conditional or range logic) must extend `test/assets.test.ts` "asset conditions, byte ranges, empty files and HEAD obey HTTP ordering" and update §1 (validators, ranges).
305
- - A new cache strategy or directive touches the `switch (strategy)` in `compile` (`src/policies/cache.ts`) and `test/policy-cache.test.ts` "each strategy emits its catalogue headers"; update §2–§5.
306
- - A new content coding touches the `codecs` table in `src/policies/compression.ts` and `test/policy-compression.test.ts` (round-trip decode table at the top of the file); update §12.
307
- - A change to the OSHP table bumps `oshpRevision` in `src/policies/security.ts`, regenerates the docs table, and must keep `test/policy-security.test.ts` "profile tables are frozen, ordered and consistent" passing; update §17–§18.
308
- - A change to the RateLimit field syntax (when the draft moves) touches `headersFor` in `src/policies/throttle.ts` and the header regexes in `test/policy-throttle.test.ts`; update §15 with the draft revision.
309
- - A change to `--trusted-proxies` parsing touches `src/client-address.ts`, `test/policies.test.ts` "client identity trusts forwarded headers only from configured proxies" and `test/standards.test.ts`; update §9 and §21.
310
- - A change to the YAML profile or schema dialect touches `parseYaml` and `validate` in `src/config.ts`, `test/config.test.ts` "YAML rejects ambiguity ..." and "strict schema rejects unknown features ..."; update §19–§20.
311
- - When a gap above is closed, delete its row from the ranked list and flip the row status in the table; when a new standard is touched, add a section with the same four columns (requirement, status, behavior citing the symbol, test name). Cite functions and constants, not line numbers: a symbol is checkable with `grep` after the file changes around it.
package/docs/STARTERS.md DELETED
@@ -1,83 +0,0 @@
1
- # One starter, two examples
2
-
3
- There is one starting project: a URL that runs a function and a regular redirect.
4
- No `dynamic` versus `redirects` choice, and no `--template` option.
5
-
6
- With URLCode installed:
7
-
8
- ```sh
9
- urlcode init ../my-links
10
- urlcode dev --project ../my-links
11
- urlcode audit --project ../my-links --expect-routes 2
12
- ```
13
-
14
- Or use the public GitHub template, which includes a pinned runtime dependency:
15
-
16
- ```sh
17
- git clone https://github.com/jimhoyd-com/urlcode-template.git my-links
18
- cd my-links
19
- npm ci
20
- npm run dev
21
- npm run audit
22
- ```
23
-
24
- GitHub's **Use this template** button creates your own repository directly.
25
- These are two ways to obtain the same route examples, not two project types.
26
- The CLI copies app files from `starters/default` and uses the installed runtime;
27
- the public repository adds npm dependency/lockfile/CI for independent installation.
28
- Neither path forks the runtime or needs a hosting account or database.
29
-
30
- ## Files and growth
31
-
32
- `urlcode.yaml` includes a function route file and a redirect file in a nested
33
- folder. A JavaScript function, HTTP assertions and optional Makefile are included,
34
- plus `.github/workflows/urlcode.yml`, which runs the [project checks action](CI.md)
35
- on every push and pull request once the project is on GitHub.
36
- See [organization](ORGANIZATION.md) for choosing your own layout. Defaults allow
37
- GET/HEAD and use redirect 302; add configuration only when changing behavior.
38
-
39
- `urlcode init` writes no `package.json`: the route project is route-only, and its
40
- runtime may be installed globally, in a parent workspace or in a container. Add
41
- `--manifest` to also write one pinning the runtime at exactly the version that
42
- generated the project, then run `npm install` in it yourself to install that
43
- version and produce a lockfile. The CLI never runs a package manager, and no
44
- upgrade command exists — a pinned version changes when you edit the manifest.
45
-
46
- Initialization refuses an existing destination. Own the app in your own repository,
47
- keep secrets out of Git, and upgrade the runtime separately without regenerating
48
- application files. Add pages, downloads, more functions and business-specific
49
- features to this same project. Update tests and the expected route count as it grows.
50
- See [readiness](READINESS.md) and [security](FUNCTION-SECURITY.md).
51
-
52
- To start an extended site instead, install the compatible core, UI, auth and
53
- admin set from npm in the directory you run from; the
54
- [framework guide](FRAMEWORK.md#the-composition-contract) provides the exact
55
- `0.4.1` command to use after publication. Pass UI first so its kit activates
56
- before auth: `urlcode init ../my-site --with ui,auth,admin` writes the same starter under `my-site/app/`, merges each package's
57
- routes and declarations into it, and generates one `host.mjs` and README beside
58
- it, plus a `package.json` pinning the runtime, those packages and their declared
59
- peers at the versions it just resolved, validated together against every declared
60
- peer range. Installing them is your explicit `npm install` in that directory.
61
- The contract each package fulfils is in [extensions](EXTENSIONS.md#scaffolding-with-init---with),
62
- with `--no-manifest` and `--pin` in [recorded versions](EXTENSIONS.md#recorded-versions).
63
-
64
- Both paths carry an `AGENTS.md` for repository-aware assistants. `urlcode init`
65
- generates it from the installed runtime's capability catalog (the same source as
66
- `urlcode capabilities`), so it names only the handlers, policies and site keys
67
- that version implements, plus the exact `validate`, `test` and `audit` commands
68
- with the starter's route count. The committed copy in `starters/default` is
69
- regenerated from the same function and a test keeps the two identical. The file
70
- points at the agent skill the package ships at `skills/urlcode/SKILL.md`.
71
- Both paths also write `.mcp.json`, which registers the read-only `urlcode mcp`
72
- server for Claude Code and Codex (`--project app` for an extended site); it is
73
- never overwritten and carries no `--allow-authoring` ([tooling](TOOLING.md#registering-the-server)).
74
-
75
- `examples/assets` contains a larger runnable file/page/download demonstration:
76
- `make dev PROJECT=examples/assets`. It is an example, not a separate init choice.
77
-
78
- The old `starter-dynamic` and `starter-redirects` branches are historical snapshots;
79
- use `urlcode-template` for new clones. They are no longer maintained or advertised
80
- as onboarding paths. Existing projects remain ordinary valid URLCode apps.
81
-
82
- The runtime is licensed under Apache-2.0. Provider adapters follow the
83
- [roadmap](../ROADMAP.md).
package/docs/STATIC.md DELETED
@@ -1,105 +0,0 @@
1
- # Deploying to static hosting (S3 + CloudFront)
2
-
3
- The `static` target is the bottom rung of the [capability
4
- ladder](FRAMEWORK.md#the-ladder):
5
- the same `urlcode.yaml` a self-hosted server or a serverless adapter runs, with
6
- no server process at all. `urlcode build --target static` compiles a project
7
- ahead of time into plain files and two small JSON manifests meant for an S3
8
- bucket served through CloudFront — nothing here executes a request.
9
-
10
- ```sh
11
- urlcode build --target static --project . --out dist --origin https://links.example
12
- ```
13
-
14
- ## Declarative routes only, and less than that
15
-
16
- This target serves **redirects**, **declared responses** (`respond:`) and
17
- static files (`page`, `static`, `download`) — no path parameters, no query
18
- passthrough or mapping, both GET and HEAD required, `respond.status: 200` only, and no `enabled: false` or `expires`
19
- (there is no server to answer a disabled or expired route with 404/410, so the
20
- build refuses one instead of silently serving it forever). Everything else is
21
- refused **at build time**, with the route pattern and the reason named:
22
-
23
- | Handler / feature | Why it is refused |
24
- | --- | --- |
25
- | `function`, `middleware` | no server, so no dynamic or sandboxed execution |
26
- | `extension`, `policies.extensions` | no server, so no operator extension registry |
27
- | `proxy`, `signals` | no server, so no bounded or fire-and-forget egress |
28
- | `conditional`, `match` (`conditions`) | no server, so no request-time condition matching |
29
- | `parameters`, `request.body` | no server, so no request-time validation |
30
- | `response.headers` | no server, so no per-request headers; set them as S3 object metadata or a CloudFront response headers policy instead |
31
- | `env`, `secrets` (`bindings`) | no server, so no per-request binding resolution |
32
- | every `policies.*` | no server, so no runtime policy enforcement |
33
- | a redirect with a `{parameter}` in its path | S3's per-object redirect is keyed to one exact object, not a pattern |
34
- | a redirect with `query.pass`/`query.map` | S3's per-object redirect cannot compute a target per request |
35
- | a redirect with a `status` other than 301 | S3's per-object website redirect always answers 301 |
36
- | a route not admitting both GET and HEAD, or admitting other methods | the output cannot enforce a different method set |
37
- | `respond.status` other than 200 | an uploaded response object cannot preserve a custom HTTP status |
38
- | a route with `enabled: false` or `expires` | no server to answer a disabled/expired route; remove the route instead |
39
-
40
- Run `urlcode capabilities --target static` for the full catalog.
41
-
42
- ## What the build emits
43
-
44
- `--out` (default `dist/static`) receives:
45
-
46
- - `objects/<key>` — the actual files to upload, one per served route. A `page`
47
- or `download` route becomes one file; a `static` mount becomes one file per
48
- entry in its directory. Object keys match the S3 convention: the route
49
- pattern with its leading `/` stripped, and `/` itself becomes `index.html`
50
- (matching a bucket's own "Index document" setting). A redirect route also
51
- gets a zero-byte placeholder object at its key, so it exists to carry
52
- metadata.
53
- - `objects.json` — `{ format, objects: [{ key, contentType, cacheControl?,
54
- contentDisposition? }] }`. Plain file upload loses this metadata (S3 does not
55
- reliably infer content type or cache headers from an extension-less key), so
56
- a deploy step reads this file and sets each object's own `Content-Type`,
57
- `Cache-Control` and `Content-Disposition` from it, for example with
58
- `aws s3 cp --content-type ... --cache-control ...` per entry, or an
59
- equivalent `aws s3 sync` post-processing step.
60
- - `redirects.json` — `{ format, redirects: [{ key, location, status: 301 }] }`.
61
- Each entry is applied as the S3 object's
62
- [website redirect location](https://docs.aws.amazon.com/AmazonS3/latest/userguide/how-to-page-redirect.html)
63
- metadata (`x-amz-website-redirect-location`), for example
64
- `aws s3api put-object --website-redirect-location <location> --bucket ... --key <key>`.
65
- S3 always answers a request for that key with a 301 to `location`
66
- regardless of the object's own content — this is why a declared `status`
67
- other than 301 is refused rather than silently downgraded.
68
- - `manifest.json` — the same project-level semantic manifest every target
69
- writes (see [tooling](TOOLING.md)).
70
-
71
- None of these are edited by hand, and `dist/` belongs in `.gitignore`. This
72
- target ships no runtime import (there is nothing to run), so there is no
73
- `@jimhoyd/urlcode/static` entry point — the build output is the deployment.
74
-
75
- ## What this is not: GitHub Pages
76
-
77
- **This target's claimed support is S3 + CloudFront, not GitHub Pages.**
78
- GitHub Pages has no per-object redirect metadata and no server-side rewrite
79
- layer, so a `redirect` route compiled the way this target compiles it (S3
80
- object metadata) has no equivalent there — the only options are a
81
- meta-refresh/JavaScript page (visibly a redirect, not an HTTP one: search
82
- engines, curl and anything that does not run JavaScript sees the source page,
83
- not a 3xx) or a static 404-page trick, both lower fidelity than what every
84
- other target in this project does for the same route. Rather than claim a
85
- portability promise this platform cannot keep, GitHub Pages is out of scope
86
- for this target. If you need Pages, treat it as a distinct target with its own
87
- explicit fidelity caveat, not a rename of this one.
88
-
89
- ## Verification status
90
-
91
- This target has local build tests only (`test/static.test.ts`): the compiled
92
- object layout, the redirect manifest, and every refusal above. **It has not
93
- been deployed to S3 or fronted by CloudFront.** Bucket policy, CloudFront
94
- caching behavior, TLS/domain setup and the exact `aws s3` invocations above are
95
- unverified until a real deployment exercises them.
96
-
97
-
98
- ## Building pages with middleware
99
-
100
- Use [prerendering](PRERENDER.md) to execute functions and native middleware at
101
- build time, then export the generated native page routes with this target.
102
- Trusted Node execution is the build default; `sandbox: true` retains its
103
- restricted imports and resource limits. Neither mode adds a request-time server
104
- to the static output. Authentication, request-dependent headers and other
105
- per-request middleware cannot be baked into a public file safely.