@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
@@ -1,169 +0,0 @@
1
- # Compression policy
2
-
3
- `policies.compression` negotiates a content coding (RFC 9110 §12.5.3) on the
4
- host, outside function/middleware execution — trusted or sandboxed alike — for
5
- every result a route produces: `respond` bodies, function results, asset
6
- responses, cache hits and early denials.
7
- It runs last in the response phase, after the cache store and the security
8
- headers, so every header it reads is final. Asset snapshots are compressed
9
- once at load and served by reference (the NGINX `gzip_static` / Caddy
10
- `precompressed` model); other bodies are compressed on the request path
11
- within a fixed size bound.
12
-
13
- ## YAML
14
-
15
- ```yaml
16
- version: "1"
17
- policies:
18
- compression:
19
- encodings: [br, gzip] # preference order; br | gzip | deflate | zstd
20
- minBytes: 1024 # bodies below this stay identity (default 1024)
21
- types: # media types eligible; `type/*` wildcards allowed
22
- - text/*
23
- - application/json
24
- - application/javascript
25
- - application/xml
26
- - image/svg+xml
27
- - application/manifest+json
28
- - application/ld+json
29
- level: 6 # 1–11, optional; mapped per codec (table below)
30
- allowWithSecrets: false # compress even where BREACH applies (default false)
31
-
32
- routes:
33
- /api/report:
34
- function: { source: functions/report.mjs }
35
- secrets: { KEY: { secret: api-key } }
36
- policies:
37
- compression: { allowWithSecrets: true } # route keys merge over project keys
38
- ```
39
-
40
- `encodings`, `minBytes`, `types`, `level` and `allowWithSecrets` are the
41
- only keys; the list above is the default `types`. `zstd` is accepted only
42
- when the running Node exposes `zlib.zstdCompressSync` (Node 22.15+ / 23.8+);
43
- otherwise startup fails with the route named, rather than silently serving
44
- identity. YAML `response.headers` cannot set `Content-Encoding` (it is a
45
- runtime-owned header); this policy is the only thing that sets it.
46
-
47
- ### Levels
48
-
49
- Without `level`, each codec uses a latency-leaning level for request-path
50
- bodies and a size-leaning level for asset variants that are computed once.
51
- With `level`, one number in YAML is mapped onto each codec's own scale and
52
- applies to both paths.
53
-
54
- | Coding | Dynamic default | Stored default | `level` mapping |
55
- |---|---|---|---|
56
- | `br` | quality 4 | quality 9 | `level` (1–11) |
57
- | `gzip` | 6 | 9 | `min(level, 9)` |
58
- | `deflate` | 6 | 9 | `min(level, 9)` |
59
- | `zstd` | 3 | 12 | `min(level × 2, 22)` |
60
-
61
- ## Negotiation
62
-
63
- The `Accept-Encoding` field is parsed per RFC 9110: each coding carries a
64
- `q` weight (default 1, `q=0` means not acceptable), `*` supplies the weight
65
- of any coding not named, and a coding neither named nor covered by `*` is
66
- not acceptable. The acceptable coding in `encodings` with the highest weight
67
- wins; equal weights are broken by the order of `encodings`, so
68
- `Accept-Encoding: gzip, br` selects `br` when the project lists `br` first.
69
- When nothing in `encodings` is acceptable, including `identity;q=0` or
70
- `*;q=0`, the response is sent as identity; a 406 helps nobody.
71
-
72
- Every response whose media type is in `types` gets `Vary: Accept-Encoding`,
73
- merged into an existing `Vary` without duplication and preserved on 304 and
74
- 206, so a shared cache keys the URL by the field even when this particular
75
- response was not compressed.
76
-
77
- ## When compression is skipped
78
-
79
- The response is sent as identity, still with `Vary`, when any of these hold:
80
-
81
- - the status is 206 (a byte range of the identity representation), 304, 204
82
- or 205, or the result already carries `Content-Encoding`;
83
- - `Cache-Control` contains `no-transform`;
84
- - the media type is not in `types`, or the body is below `minBytes`;
85
- - the request method is `HEAD` and the body has no precomputed variant (see
86
- below);
87
- - the body exceeds 1 MiB and has no precomputed variant: a synchronous
88
- compression of that size would hold the event loop for every other
89
- request, so such bodies stay identity on the request path;
90
- - compressing did not shrink the body;
91
- - **BREACH:** the route declares `secrets`, or the response carries
92
- `Set-Cookie`, unless `allowWithSecrets: true`.
93
-
94
- ### BREACH rationale
95
-
96
- Compressing a body that mixes a secret (a session token, a CSRF token, an
97
- API key echoed into a page) with attacker-influenced input leaks the secret
98
- through the compressed length: an attacker who can make the victim's
99
- browser issue requests with chosen input measures which guesses shrink the
100
- response. The signal is only there when secret and input share a
101
- compressed body, so the policy refuses to compress exactly where a secret is
102
- plausible: a route that has been granted secrets, and any response that
103
- sets a cookie. `allowWithSecrets: true` is for routes whose bodies do not
104
- echo the secret (a route that uses a key to call an upstream API and returns
105
- public data); set it per route, not for the project.
106
-
107
- ## ETag and HEAD
108
-
109
- RFC 9110 requires a strong validator to differ between representations, so
110
- an encoded body cannot carry the identity ETag unchanged. The policy handles
111
- the two paths differently:
112
-
113
- - **Precompressed assets** keep a strong ETag with the coding appended
114
- inside the quotes: `"<sha256>-br"`, `"<sha256>-gz"`, `"<sha256>-df"`,
115
- `"<sha256>-zs"`. The asset handler validates `If-None-Match` against the
116
- identity tag; the policy validates the suffixed tag for the coding it
117
- selected and answers 304 (with the suffixed ETag and `Vary`) when it
118
- matches. A suffixed tag presented with a different `Accept-Encoding`
119
- selects a different representation and gets a fresh 200. `If-Range` only
120
- ever matches the identity tag, so a range request against a variant tag
121
- gets the full identity body, as the RFC prescribes for a non-matching
122
- validator.
123
- - **Dynamically compressed bodies** (functions, `respond`, middleware
124
- results, cache hits) keep the handler's ETag but weakened: `W/"v1"`. Weak
125
- comparison treats `W/"v1"` and `"v1"` as equal, so a client revalidating
126
- with the weak tag gets the same 304 it would for identity, and a
127
- handler's own `If-None-Match` logic keeps working. Nothing is appended,
128
- because a dynamic body has no stable bytes for a strong tag to name.
129
-
130
- `HEAD` reports what `GET` would send when the answer is free: a
131
- precompressed asset answers `HEAD` with `Content-Encoding` and the variant's
132
- `Content-Length`. A dynamic body is not compressed for `HEAD` (it would pay
133
- the whole compression for one number) and reports the identity length,
134
- the same `Content-Length` its `GET` would carry uncompressed.
135
-
136
- ## Precompression and memory bounds
137
-
138
- `compileAssets` runs before policies compile, so the policy, not the asset
139
- loader, computes variants: in `compile()` it walks the route's asset
140
- snapshot (one file for `page`/`download`, the whole tree for `static`) and
141
- compresses every file whose type is in `types` and whose size is at least
142
- `minBytes`, once per configured coding. A project without the policy pays
143
- nothing. Variants are stored on the immutable snapshot and replaced with it
144
- on reload. Bounds:
145
-
146
- - a variant at least as large as the original is dropped;
147
- - the aggregate of all variants across the runtime is capped at 64 MiB
148
- (the same figure as the asset snapshot itself); beyond it, remaining
149
- files are served identity or compressed on the request path when they
150
- fit the 1 MiB dynamic bound;
151
- - a request for a stored variant costs one buffer reference, no copy.
152
-
153
- `urlcode audit` and `testPlan()` report `precompressed`, the number of
154
- variants a route holds, alongside `encodings`, `minBytes`, the count of
155
- `types` and `level`.
156
-
157
- ## Per-target behavior
158
-
159
- | Target | Support | Notes |
160
- |---|---|---|
161
- | node | native | Negotiation, precompressed assets and dynamic compression as described. |
162
- | vercel | delegated | The platform compresses responses at its edge; the policy is accepted and dropped so one YAML serves every host. |
163
- | aws | delegated | CloudFront/API Gateway compression is configured on the platform; the policy is accepted and dropped. |
164
- | cloudflare | delegated | Workers responses are compressed by the Cloudflare edge; the build accepts and drops the policy. |
165
-
166
- A refusal is deliberate: the YAML stays portable and the difference is
167
- visible at build time rather than as a silent double compression. Remove
168
- the key, or set `compression: false` on the routes that use a profile which
169
- declares it, when deploying to those targets.
@@ -1,52 +0,0 @@
1
- # Policies: The policy contract and your own patterns
2
-
3
- Part of [policies](../POLICIES.md), which holds the shared rules and the per-target table.
4
-
5
- ## The policy contract in TypeScript
6
-
7
- The policies are modules of one shape, `PolicyModule<Config, State>` in
8
- `src/types.ts`: `targets`, `compile`, `onRequest`, `onResponse`, optional
9
- `onError`, `describe` and `close`. `@jimhoyd/urlcode/policies` exports that type with
10
- `PolicyRequest`, `PolicyContext`, `PolicyChain`, `PolicyShared` and
11
- `PolicyRegistry` (the five built-ins keyed by name), and the declarations ship
12
- with the package. A per-policy configuration is typed as the YAML it accepts,
13
- and a `profiles` layer may hold a partial one:
14
-
15
- ```ts
16
- import { registry, targets, type PolicyRegistry, type PolicyRequest } from '@jimhoyd/urlcode/policies';
17
-
18
- const throttle: PolicyRegistry['throttle'] = registry.throttle; // PolicyModule<ThrottleConfig, ThrottleState>
19
- const support = throttle.targets({ quota: 120, window: 60, partition: 'client', status: 429 }); // per-target support for this config
20
- console.log(support.vercel, support.cloudflare);
21
- function inspect(request: PolicyRequest): void { console.log(request.route, request.client, targets); }
22
- ```
23
-
24
- The registry is read-only: a project cannot add a policy from YAML, and an
25
- operator adds behavior through [plugins](../PLUGINS.md), not by editing it.
26
-
27
- ## Supplying your own patterns
28
-
29
- The runtime ships mechanisms and one named profile, not an opinion about who
30
- should be blocked. Ways to express your own:
31
-
32
- - **Custom profiles.** Define any number under `profiles` and select one per
33
- project or per route. Profiles are plain data and travel with the YAML.
34
- - **Per-route overrides.** Any key can be tightened, replaced or set to
35
- `false` on a route.
36
- - **Own agent lists.** `agents.deny` and `agents.allow` accept bundled list
37
- names and project-relative `.json` files in the same schema, so a list you
38
- do not want to redistribute stays yours. `denyPatterns`/`allowPatterns`
39
- take a bounded, linear-time pattern subset. See [agents](agents.md).
40
- - **Header by header.** `security.set` adds or overrides a header and wins over
41
- the profile, YAML `response.headers` and handler output; `security.unset`
42
- drops one the profile would emit. Headers the runtime or a handler owns
43
- (`content-type`, `cache-control`, `set-cookie`, `etag`, `location`, and the
44
- rest listed in `src/policies/security.ts`) cannot be `set`.
45
- See [security](security.md).
46
- - **Explicit cache fields.** A strategy sets defaults; `maxAge`,
47
- `staleWhileRevalidate`, `staleIfError`, `cdnMaxAge`, `originTtl`, `vary`,
48
- `statuses`, `maxBytes` and `maxEntries` override what it implies.
49
- See [cache](cache.md).
50
- - **Plugins.** Verified-bot checks, shared-store throttling, purge endpoints
51
- and anything vendor-specific are host code an operator passes in;
52
- see [plugins](../PLUGINS.md).
@@ -1,56 +0,0 @@
1
- # Policies: The built-in `hardened` profile and hardening guidance
2
-
3
- Part of [policies](../POLICIES.md), which holds the shared rules and the per-target table.
4
-
5
- ## The built-in `hardened` profile
6
-
7
- `policies.profile: hardened` expands to the following and nothing else, so it
8
- can be read in one place and overridden key by key. This is
9
- `builtinProfiles.hardened` in `src/policies.ts`:
10
-
11
- ```yaml
12
- policies:
13
- security: { headers: oshp }
14
- agents: { deny: [ai-crawlers], status: 403 }
15
- throttle: { quota: 120, window: 60, partition: client, status: 429 }
16
- compression: { encodings: [br, gzip], minBytes: 1024 }
17
- cache: { strategy: revalidate }
18
- ```
19
-
20
- The numbers are starting points chosen to be safe for a single small instance;
21
- they are not tuned for any workload and not a security assessment of your
22
- deployment. Note what the profile implies per target: on Vercel and AWS it
23
- refuses activation as written, because `throttle.partition: client` is
24
- refused there; set `throttle: false` (or `partition: route`) at the project
25
- or route level. `compression` is delegated to the platform. On Cloudflare
26
- only `agents` and `security` survive and `compression` is delegated, so the
27
- profile must also drop `throttle` and `cache` there.
28
- There is no `strict` profile: anything stricter is a per-project decision.
29
-
30
- ## Hardened configuration guidance
31
-
32
- Advice, not defaults, condensed from the spike's section 6.
33
-
34
- 1. **Network and edge first.** Volumetric protection, TLS termination and
35
- per-client connection budgets stay with the provider or the reverse proxy.
36
- Runtime policies are a second layer, never the first.
37
- 2. **Ingress to origin.** Bind privately; allow only the proxy's addresses;
38
- pass `--trusted-proxies` so `client` partitioning sees the real peer.
39
- 3. **Request policies.** Agents before throttle: denials are cheaper than
40
- counting. Start throttle in `mode: report` for a release to see real
41
- quotas in the headers and logs, then switch to `enforce`.
42
- 4. **Allow before deny.** Keep an explicit allow for the crawlers you need
43
- indexed; a broad deny without one is the common self-inflicted outage.
44
- 5. **Route contract.** Exact methods, `request.body` limits and `expires` on
45
- campaign routes still do most of the work.
46
- 6. **Response policies.** Security headers on every route; compression only
47
- on listed types and never on secret-bearing responses (the BREACH class of
48
- attack, which is why compression is skipped where a route declares secrets
49
- unless `allowWithSecrets` says otherwise); caching only with a strategy
50
- whose semantics you can state, `immutable` only on content-hashed paths,
51
- `no-store` everywhere else.
52
- 7. **Lists as pinned data.** Bundled agent lists ship with the release, so a
53
- rollback rolls the list back too.
54
- 8. **Read the table.** Check `urlcode routes` on each target you deploy to;
55
- the same YAML is refused where it cannot be enforced, and that is the
56
- point.
@@ -1,169 +0,0 @@
1
- # Policies: Interoperability between policies
2
-
3
- Part of [policies](../POLICIES.md), which holds the shared rules and the per-target table.
4
-
5
- ## Interoperability
6
-
7
- Every pair of policies, and every policy against the runtime's own responses,
8
- was reviewed by reading the code and by exercising a server. The rules below
9
- are the ones the current code enforces; where a combination misbehaves, the
10
- rule says so and names what to do instead.
11
-
12
- ### Cannot coexist
13
-
14
- Hard conflicts: the runtime refuses activation, or the combination produces
15
- a wrong answer today.
16
-
17
- - **Origin cache and conditional or range requests.** A request carrying
18
- `If-None-Match`, `If-Modified-Since`, `If-Match`, `If-Unmodified-Since` or
19
- `Range` is never answered from the origin cache: a stored entry is a full
20
- `200` representation and the handler owns validators and ranges, so such
21
- requests always reach it and get the handler's `304`, `412` or `206`.
22
- Unconditional requests are served from the entry.
23
- - **`security.unset` and a route that switches profile.** `unset` is
24
- validated against the profile in effect on that route. A project-level
25
- `unset` merges into a route that names a profile without that header
26
- (`headers: off`, or `oshp-no-csp` with `unset: [Content-Security-Policy]`)
27
- and activation fails with `unset names "...", which the off profile does
28
- not emit`. Write `unset: []` on that route: a route key replaces the whole
29
- array.
30
- - **`security.set` of `Cache-Control`, `Content-Encoding`, `ETag`,
31
- `Content-Type`, `Set-Cookie`, `Location` and the other reserved names** is
32
- refused at activation (`is owned by the runtime or handler`). `Vary`,
33
- `RateLimit`, `RateLimit-Policy`, `Retry-After` and `Age` are reserved for
34
- the same reason: the cache and throttle policies own them, and a `set` of
35
- any of the five is refused at activation with the route named. Declare
36
- `vary` on the cache policy instead.
37
- - **`agents.denyEmpty`, `throttle` and the generated probes.** `urlcode
38
- audit`, `urlcode test` and `urlcode benchmark` send generated probes as
39
- `Mozilla/5.0 (compatible; RouteProbe/0.1)`,
40
- so `denyEmpty` does not fail them; a deny pattern that matches that string
41
- would. A tight `throttle` fails an audit once the probes exceed `quota`
42
- (the audit runs from one address, so a `static` tree with more files than
43
- the quota trips it): keep the quota at least the number of generated cases
44
- or run the audit in `mode: report`.
45
- - **`hardened` on Vercel and AWS** is refused as written, because
46
- `throttle.partition: client` cannot be honoured there; **on Cloudflare**
47
- the build refuses `throttle` and `cache`. Override the offending keys with
48
- `false` or `partition: route` as [the `hardened` page](hardened.md) describes.
49
- - **`compression.encodings: [zstd]` on a Node without `zlib.zstdCompressSync`**
50
- fails activation with the route named rather than serving identity.
51
-
52
- ### Coexist with defined precedence
53
-
54
- Pairs that work, with the rule the code applies.
55
-
56
- - **Request order is `agents`, `throttle`, cache lookup.** A denied agent is
57
- never counted and never looked up; a throttled request is never looked up.
58
- An agent on an `allow` list passes `agents` and is still throttled. An
59
- `agents` denial and a `throttle` refusal both pass through the cache
60
- policy's response hook (they carry no flight, so nothing is stored) and
61
- pick up its declared `vary` names, then security headers, then
62
- `Vary: Accept-Encoding` from compression; their bodies are below `minBytes`
63
- so they are never encoded.
64
- - **Cache hit and the response phase.** A hit skips only the cache's own
65
- response hook: it still carries the client's `RateLimit` headers (the
66
- request was counted), the security profile, and is compressed on the way
67
- out. Stored entries are the handler's bytes and headers after YAML
68
- `response.headers` and the cache policy ran, before throttle, security and
69
- compression: bodies are stored uncompressed and encoded again on every hit,
70
- and a `RateLimit` value is never stored. A cache hit on an asset serves the
71
- same snapshot buffer, so a precompressed variant is used for `GET` and
72
- reported at the variant's length for `HEAD`, exactly as when uncached.
73
- - **Cache `Vary` and compression `Vary`.** The cache merges its declared
74
- `vary` names first; compression appends `Accept-Encoding` without
75
- duplicating it and leaves a `Vary: *` alone. The origin key uses only the
76
- cache's names: because bodies are stored uncompressed, `Accept-Encoding` is
77
- not part of the key and need not be.
78
- - **ETags across `cache` and `compression`.** `revalidate` keeps a handler or
79
- asset `ETag` and computes a strong SHA-256 tag for a `200` without one.
80
- Compression then weakens a dynamically encoded body's tag (`W/"..."`) and
81
- suffixes a precompressed asset's (`"...-gz"`). Revalidation works in every
82
- combination: the cache's `304` compares weakly, the asset handler compares
83
- weakly against the identity tag, and compression answers `304` for a
84
- suffixed tag it produced. The `304` that `revalidate` produces keeps
85
- `Content-Type`, so compression adds `Vary: Accept-Encoding` to it as it
86
- does to the asset handler's own `304`.
87
- - **Who owns `Cache-Control`.** YAML `response.headers` first, then an asset
88
- handler's `cacheControl` when the cache policy is only inherited, then a
89
- handler's `private` or `no-store`, then the strategy; `security.set` cannot
90
- name it. A `no-store` or `private` handler answer is never stored whatever
91
- the strategy says.
92
- - **Security headers and everything else.** Profile headers fill gaps only:
93
- YAML `response.headers`, function, asset, redirect and early-denial headers
94
- keep their values. `set` overrides all of them. `Strict-Transport-Security`
95
- needs an `https` origin. The profile is applied on cache hits and on early
96
- denials (including a plugin short-circuit), on the self-hosted server and
97
- in the Worker alike.
98
- - **Compression and secrets or cookies.** A route with `secrets` or a
99
- response with `Set-Cookie` is sent as identity (still with `Vary`) unless
100
- `allowWithSecrets: true`. A route with `secrets` also never enters the
101
- origin cache, so the two policies agree on what a secret-bearing route is.
102
- - **Throttle and the 405.** The request phase runs before the method check,
103
- so a request that ends as `405` was counted, and the `405` passes through
104
- the response phase: it carries the `RateLimit` headers, the security
105
- profile and plugin `onResponse` rewrites. Nothing stores it.
106
- - **Plugins and policies.** `onRequest` runs before every policy; a
107
- short-circuit result skips `agents`, `throttle` and the cache lookup
108
- (nothing is counted, matched or stored) and skips the cache store and the
109
- throttle headers, then receives security headers and compression, then
110
- every plugin's `onResponse`. On a thrown error the policies' `onError`
111
- hooks run first (in the order the policies were declared) and then plugin
112
- `onError` hooks in reverse. Only the cache policy has an error hook today
113
- and it never returns a fallback (`stale-if-error` is header-only); if a
114
- future policy did, that fallback would pass through the response phase and
115
- every plugin `onResponse`, and plugin `onError` would not run for that
116
- request.
117
- - **Reload.** Every reload builds a new runtime with fresh shared state:
118
- throttle counters start empty and the origin cache and precompressed
119
- variants are rebuilt; a client mid-window gets a fresh budget. The plugin
120
- objects are the ones the operator passed and persist across reloads:
121
- `onActivate` runs for the new runtime before `onClose` runs for the old,
122
- so a plugin's own state (a shared-store connection, a `WeakMap`) survives a
123
- reload unless its `onClose` discards it. A plugin whose `onActivate`
124
- throws makes the reload fail and the old runtime keeps serving.
125
- - **Merge edge cases.** A route `profile` that lacks a key the project layer
126
- had leaves that key in force (layers merge, they do not replace); `false`
127
- at the project level followed by an object at the route level re-enables
128
- the policy with the route's object alone, so it must be complete
129
- (`throttle` needs `quota` and `window`); a custom profile named like a
130
- built-in shadows it everywhere.
131
- - **Memory bounds add up.** Per runtime: 64 MiB of asset snapshot, up to
132
- 64 MiB of precompressed variants, up to 64 MiB of origin-cache bodies (a
133
- stored asset references the snapshot buffer rather than copying it, but
134
- is counted against the cache budget), and the throttle table at the
135
- largest `maxKeys` (100,000 keys by default). Two routes serving the same
136
- file compress it separately and both count. During a reload both
137
- generations exist at once. [Capacity](../CAPACITY.md) states the asset,
138
- throttle and cache figures; the precompressed budget belongs in that table
139
- too.
140
- - **Cloudflare.** The artifact carries the effective `agents` and `security`
141
- configuration per route, with the entries of any project list file
142
- embedded under the reference as written in YAML (no filesystem path); a
143
- route-level `agents: false` leaves the artifact without that key. The
144
- Worker runs `agents` on the request and `security` on the response, so an
145
- early denial carries the profile exactly as on the self-hosted server.
146
-
147
- ### Not covered by policies
148
-
149
- - **Every error the runtime throws** (404 for no match, disabled route or
150
- missing link, 410, 400/413/415 from body checks, 502/503/504 from the
151
- sandbox or a link store) bypasses the response phase: no `Vary`, no
152
- `RateLimit`, no compression, no plugin `onResponse`, on every target.
153
- What they do get is the `security` policy: the matched route's effective
154
- profile when the error came after routing (so a route with
155
- `security: false` answers its 410 bare), otherwise the project-level
156
- profile, including a host-side error such as an oversized body or shed
157
- admission and the Worker's own 404. The runtime's fixed headers
158
- (`Content-Type`, `Cache-Control: no-store`, `Content-Length`,
159
- `X-Request-Id`, `X-Content-Type-Options`) can never be replaced by it.
160
- Plugins keep `onError` for observation; a policy error hook may answer with
161
- a fallback, and none does today.
162
- - **The audit's probes** share one address and one `User-Agent`, so they do
163
- not exercise `agents` or `throttle` the way real traffic does;
164
- the policy table in `testPlan().policies` is the audit's evidence for
165
- those two.
166
- - **`stale-if-error` at the origin**: header-only, as the
167
- [cache page](cache.md) states.
168
- - **Cross-instance state**: counters and the cache are per runtime on every
169
- target; a shared budget or a shared cache is a plugin.
@@ -1,45 +0,0 @@
1
- # Policies: Client identity, inventory and logging
2
-
3
- Part of [policies](../POLICIES.md), which holds the shared rules and the per-target table.
4
-
5
- ## Client identity and `--trusted-proxies`
6
-
7
- `throttle` partitions by `client`. On the self-hosted server the client is the
8
- socket peer unless `urlcode serve --trusted-proxies 10.0.0.0/8,fd00::/8`
9
- names the addresses allowed to speak for a client. Then `X-Forwarded-For` is
10
- walked from the right, skipping trusted hops, and the first untrusted address is
11
- the client; a chain made only of trusted proxies yields its leftmost entry, and
12
- a malformed entry is skipped. A forwarded header from a peer
13
- outside the trusted set is ignored, as is a request carrying more than one
14
- `X-Forwarded-For` field. Ranges are IPv4 or IPv6 CIDRs (at most 256);
15
- IPv4-mapped IPv6 peers match IPv4 ranges. `startServer({ trustedProxies })`
16
- takes the same list.
17
-
18
- A request whose client cannot be resolved (an adapter without a peer, an
19
- embedding caller that passes none) shares one bucket rather than being exempt,
20
- so a misconfigured proxy fails closed. The throttle summary in
21
- `testPlan().policies` records this as `unresolvedClient: "shared key"`. The runtime still
22
- never trusts forwarded headers for its public origin; set `--origin`
23
- explicitly, as [resilience](../RESILIENCE.md) already requires.
24
-
25
- ## What `routes` and `audit` report
26
-
27
- `urlcode routes` prints the inventory with a `policies` array per route naming
28
- the policies effective on it (`testPlan().inventory[].policies`) and the full
29
- `policies` map. The embedding API and a plugin's `onActivate` see
30
- `testPlan().policies`, a map from route pattern to each policy's summary with
31
- its `target` value (`native`, `compiled` or `delegated`), the per-route
32
- capability table the portability rule calls for. `urlcode audit` prints the
33
- same table under `policies` and, with `--compliance`, checks the declared
34
- configuration against standards-referenced rules; see
35
- [compliance](../COMPLIANCE.md). `urlcode doctor` lists the policy names this
36
- runtime knows.
37
-
38
- ## Logging
39
-
40
- Policies log through the runtime's request log with one-line events:
41
- `{ event: 'throttle', route, outcome: 'exceeded' | 'allowed', remaining }`,
42
- `{ event: 'agents', route, list, outcome: 'denied' | 'reported' }` and the
43
- cache events described on the [cache page](cache.md). Events name the
44
- configured route pattern and the list or strategy, never a client address, a
45
- User-Agent string or request text. A logging failure never changes a response.
@@ -1,161 +0,0 @@
1
- # Security headers policy
2
-
3
- `policies.security` adds response security headers on the host, outside
4
- function/middleware execution -- trusted or sandboxed alike -- to every result
5
- a route produces: redirects, `respond` bodies,
6
- function results, asset responses, cache hits and early denials from the
7
- `agents` and `throttle` policies. Values are the OWASP Secure Headers
8
- Project best-practice recommendations (OSHP 2024 best practices,
9
- <https://owasp.org/www-project-secure-headers/>). The tables below are
10
- generated from the frozen `profiles` constant in `src/policies/security.ts`.
11
-
12
- ## YAML
13
-
14
- ```yaml
15
- version: "1"
16
- policies:
17
- security:
18
- headers: oshp # oshp (default) | oshp-no-csp | off
19
- set: # add or overwrite one header, verbatim
20
- Content-Security-Policy-Report-Only: "default-src 'self'; report-to csp"
21
- Clear-Site-Data: '"cache"'
22
- unset: # drop a header the profile would emit
23
- - Cross-Origin-Embedder-Policy
24
- routes:
25
- /embed:
26
- respond: { text: ok }
27
- policies:
28
- security: { headers: oshp-no-csp } # route keys merge over project keys
29
- ```
30
-
31
- `headers`, `set` and `unset` are the only keys. A route may also write
32
- `security: false` to disable the policy for that route.
33
-
34
- ## Profile `oshp`
35
-
36
- | Header | Value |
37
- |---|---|
38
- | `strict-transport-security` | `max-age=31536000; includeSubDomains` |
39
- | `x-frame-options` | `deny` |
40
- | `x-permitted-cross-domain-policies` | `none` |
41
- | `referrer-policy` | `strict-origin-when-cross-origin` |
42
- | `content-security-policy` | `default-src 'self'; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content` |
43
- | `cross-origin-embedder-policy` | `require-corp` |
44
- | `cross-origin-opener-policy` | `same-origin` |
45
- | `cross-origin-resource-policy` | `same-origin` |
46
- | `permissions-policy` | `accelerometer=(), autoplay=(), camera=(), cross-origin-isolated=(), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(self), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), hid=(), idle-detection=(), interest-cohort=(), serial=(), unload=()` |
47
-
48
- ## Profile `oshp-no-csp`
49
-
50
- Identical to `oshp` without `Content-Security-Policy`.
51
-
52
- | Header | Value |
53
- |---|---|
54
- | `strict-transport-security` | `max-age=31536000; includeSubDomains` |
55
- | `x-frame-options` | `deny` |
56
- | `x-permitted-cross-domain-policies` | `none` |
57
- | `referrer-policy` | `strict-origin-when-cross-origin` |
58
- | `cross-origin-embedder-policy` | `require-corp` |
59
- | `cross-origin-opener-policy` | `same-origin` |
60
- | `cross-origin-resource-policy` | `same-origin` |
61
- | `permissions-policy` | `accelerometer=(), autoplay=(), camera=(), cross-origin-isolated=(), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(self), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), hid=(), idle-detection=(), interest-cohort=(), serial=(), unload=()` |
62
-
63
- ## Profile `off`
64
-
65
- Emits nothing. `set` still applies, so `off` plus `set` is a hand-written
66
- header list.
67
-
68
- Not in any profile:
69
-
70
- - `X-Content-Type-Options: nosniff`: the runtime sets it on every response,
71
- including errors, so the profile does not duplicate it.
72
- - `Cache-Control`: owned by the `cache` policy and the asset handlers;
73
- `set` refuses it.
74
- - `Clear-Site-Data`: destructive by design, so only on explicit `set`.
75
-
76
- ## Precedence
77
-
78
- 1. Headers already on the result keep their value. YAML `response.headers`
79
- are applied by the runtime before this policy runs, and a function or asset
80
- handler sets its headers earlier still, so any of them beats the profile
81
- header by header (`x-frame-options: sameorigin` on a route wins over the
82
- profile's `deny`).
83
- 2. `set` overwrites everything: the profile and an existing header of the
84
- same name. Writing a header under `set` is explicit operator intent.
85
- 3. `unset` removes a header from the profile by case-insensitive name. Naming
86
- a header the selected profile does not emit is a configuration error that
87
- names the route, so a typo cannot silently leave a header in place. The
88
- check runs against the profile in effect on each route: a route that
89
- switches to a profile without that header inherits the project `unset`
90
- and must write `unset: []` to clear it.
91
-
92
- `set` names and values are validated at activation with the same rules the
93
- wire enforces (RFC 7230 token names, no control characters) and may not name a
94
- header the runtime or a handler owns (`content-length`, `content-type`,
95
- `location`, `etag`, `content-encoding`, `cache-control`, `set-cookie`,
96
- `x-request-id`, `x-content-type-options`, hop-by-hop headers), nor one
97
- another policy emits (`vary`, `ratelimit`, `ratelimit-policy`, `retry-after`,
98
- `age`). The static
99
- headers of one route are capped at 8 KiB so the response keeps room under the
100
- runtime's 16 KiB / 256-header limit; the error names the route.
101
-
102
- ## Error responses
103
-
104
- Errors the runtime throws (404, 410, 413 and the rest) do not run the
105
- response phase, but they do get this policy: the matched route's effective
106
- profile when the error came after routing, otherwise the project-level one,
107
- on every host and in the Cloudflare Worker. The fixed error headers
108
- (`Content-Type`, `Cache-Control: no-store`, `Content-Length`, `X-Request-Id`,
109
- `X-Content-Type-Options`) are never replaced.
110
-
111
- ## HSTS and the origin
112
-
113
- `Strict-Transport-Security` is emitted only when the request origin is
114
- `https:`. On the self-hosted server that is the `--origin` setting (or the
115
- `origin` option of `startServer`); the Worker uses the request URL. A
116
- browser ignores HSTS on a plain-text response, and a forwarded header such as
117
- `X-Forwarded-Proto` is client-controlled, so the runtime never infers the
118
- scheme from it. Behind a TLS-terminating proxy or tunnel, state
119
- `--origin https://your.host` and the header appears. Once emitted, HSTS
120
- commits the host to HTTPS for a year including subdomains: set the origin only
121
- when that is true.
122
-
123
- ## CSP and pages with inline scripts
124
-
125
- The `oshp` CSP (`default-src 'self'`, no `unsafe-inline`) blocks inline
126
- `<script>` and `<style>` blocks and any third-party script. For a page that
127
- needs them, either use `oshp-no-csp` on that route, or keep the profile and
128
- trial a policy in report-only mode first:
129
-
130
- ```yaml
131
- policies:
132
- security:
133
- headers: oshp-no-csp
134
- set:
135
- Content-Security-Policy-Report-Only: "default-src 'self'; script-src 'self' https://cdn.example; report-to csp"
136
- ```
137
-
138
- Once the reports are clean, move the value to `Content-Security-Policy` under
139
- `set`, which overrides the profile value.
140
-
141
- ## Targets
142
-
143
- | Target | Support | Notes |
144
- |---|---|---|
145
- | node | native | `--origin` decides HSTS |
146
- | vercel | native | origin from the adapter's public URL |
147
- | aws | native | origin from the adapter's public URL |
148
- | cloudflare | compiled | Validated at build; the Worker compiles the same module synchronously and emits identical headers. HSTS follows the request URL scheme. |
149
-
150
- ## Interaction with other policies
151
-
152
- - `cache`: security headers are added after the cache store and on cache
153
- hits, so a stored body never carries a stale profile; `Cache-Control` is
154
- never touched.
155
- - `compression`: runs after this policy, so `Vary`/`Content-Encoding` are
156
- unaffected and the size check above already includes the profile.
157
- - `agents` and `throttle`: their denials pass through this policy, so a 403
158
- or 429 carries the same headers as a normal response.
159
-
160
- `urlcode audit` and `testPlan().policies` report, per route, the profile
161
- name, the header names it emits, and the `set` and `unset` names.