@jimhoyd/urlcode 0.4.2 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/urlcode-authoring/SKILL.md +57 -14
- package/.claude/skills/urlcode-operations/SKILL.md +4 -0
- package/README.md +42 -18
- package/SECURITY.md +5 -3
- package/dist/BUILD-MANIFEST.json +30 -27
- package/dist/agent-context.js +82 -0
- package/dist/agents-guide.js +42 -42
- package/dist/authoring.js +12 -2
- package/dist/body-schema.js +159 -0
- package/dist/build-cloudflare.js +2 -0
- package/dist/capabilities.js +1 -1
- package/dist/cli.js +28 -12
- package/dist/config.js +74 -6
- package/dist/context.js +4 -6
- package/dist/errors.js +3 -1
- package/dist/examples.js +1 -1
- package/dist/extensions.js +60 -2
- package/dist/http-policy.js +19 -4
- package/dist/http-response.js +2 -2
- package/dist/init-with.js +71 -9
- package/dist/mcp.js +15 -2
- package/dist/pattern-guard.js +32 -0
- package/dist/policies/security.js +0 -0
- package/dist/project-tests.js +35 -11
- package/dist/readiness.js +184 -32
- package/dist/recipes.js +1 -1
- package/dist/router.js +17 -0
- package/dist/runtime.js +26 -3
- package/dist/scaffold.js +0 -0
- package/dist/server.js +26 -3
- package/dist/site.js +0 -0
- package/dist/tooling.js +2 -1
- package/dist/types/agent-context.d.ts +44 -0
- package/dist/types/authoring.d.ts +3 -1
- package/dist/types/body-schema.d.ts +58 -0
- package/dist/types/config.d.ts +10 -2
- package/dist/types/context.d.ts +1 -1
- package/dist/types/errors.d.ts +9 -1
- package/dist/types/examples.d.ts +1 -1
- package/dist/types/extensions.d.ts +47 -2
- package/dist/types/http-policy.d.ts +3 -0
- package/dist/types/init-with.d.ts +10 -1
- package/dist/types/pattern-guard.d.ts +10 -0
- package/dist/types/project-tests.d.ts +9 -0
- package/dist/types/readiness.d.ts +68 -0
- package/dist/types/recipes.d.ts +1 -1
- package/dist/types/runtime.d.ts +4 -0
- package/dist/types/server.d.ts +9 -1
- package/dist/types/tooling.d.ts +1 -0
- package/dist/types/types.d.ts +13 -0
- package/dist/types.js +10 -2
- package/dist/typescript-authoring.js +5 -3
- package/dist/verify-deployment.js +16 -10
- package/examples/body-validation/README.md +16 -0
- package/examples/body-validation/example.yaml +17 -0
- package/examples/body-validation/tests/requests.json +15 -0
- package/examples/body-validation/urlcode.yaml +34 -0
- package/examples/coverage-waiver/README.md +8 -0
- package/examples/coverage-waiver/example.yaml +16 -0
- package/examples/coverage-waiver/functions/notes.mjs +2 -0
- package/examples/coverage-waiver/tests/requests.json +3 -0
- package/examples/coverage-waiver/urlcode.yaml +10 -0
- package/examples/data-dir/README.md +39 -0
- package/examples/data-dir/data/welcome.txt +1 -0
- package/examples/data-dir/example.yaml +22 -0
- package/examples/data-dir/functions/note.mjs +18 -0
- package/examples/data-dir/tests/requests.json +6 -0
- package/examples/data-dir/urlcode.yaml +6 -0
- package/examples/lifecycle/README.md +15 -0
- package/examples/lifecycle/example.yaml +19 -0
- package/examples/lifecycle/functions/notes.mjs +29 -0
- package/examples/lifecycle/tests/requests.json +12 -0
- package/examples/lifecycle/urlcode.yaml +29 -0
- package/examples/not-found/README.md +10 -0
- package/examples/not-found/example.yaml +17 -0
- package/examples/not-found/public/404.html +3 -0
- package/examples/not-found/public/index.html +3 -0
- package/examples/not-found/tests/requests.json +7 -0
- package/examples/not-found/urlcode.yaml +7 -0
- package/examples/shared-blocks/README.md +11 -0
- package/examples/shared-blocks/example.yaml +17 -0
- package/examples/shared-blocks/tests/requests.json +7 -0
- package/examples/shared-blocks/urlcode.yaml +37 -0
- package/llms-full.txt +364 -49
- package/llms.txt +44 -6
- package/package.json +15 -26
- package/recipes/static-page/README.md +9 -0
- package/recipes/static-page/public/index.html +11 -0
- package/recipes/static-page/recipe.yaml +21 -0
- package/recipes/static-page/tests/requests.json +22 -0
- package/recipes/static-page/urlcode.yaml +7 -0
- package/recipes/static-plus-api/README.md +6 -0
- package/recipes/static-plus-api/urlcode.yaml +4 -0
- package/recipes/store-crud/README.md +53 -0
- package/recipes/store-crud/recipe.yaml +31 -0
- package/recipes/store-crud/tests/requests.json +18 -0
- package/recipes/store-crud/urlcode.yaml +18 -0
- package/schemas/urlcode.schema.json +125 -60
- package/skills/urlcode/SKILL.md +47 -27
- package/starters/default/AGENTS.md +43 -43
- package/starters/page/README.md +14 -0
- package/starters/page/public/index.html +12 -0
- package/starters/page/tests/requests.json +17 -0
- package/starters/page/urlcode.yaml +6 -0
- package/.claude-plugin/marketplace.json +0 -18
- package/CONTRIBUTING.md +0 -148
- package/ROADMAP.md +0 -70
- package/docs/AI-AUTHORING.md +0 -339
- package/docs/ASSETS.md +0 -107
- package/docs/AUTH-BACKUP.md +0 -32
- package/docs/AWS.md +0 -86
- package/docs/BEST-PRACTICES.md +0 -276
- package/docs/BULK.md +0 -79
- package/docs/CAPABILITIES.md +0 -192
- package/docs/CAPACITY.md +0 -305
- package/docs/CI-FOLLOWUP-2026-09-19.md +0 -97
- package/docs/CI-RELEASE-AUDIT-2026-09-19.md +0 -322
- package/docs/CI.md +0 -147
- package/docs/CLOUDFLARE.md +0 -109
- package/docs/CODEBASE-AUDIT-2026-09-20.md +0 -284
- package/docs/COMPLIANCE.md +0 -239
- package/docs/COMPOSING-A-SITE.md +0 -287
- package/docs/CONDITIONS.md +0 -74
- package/docs/CONTAINER-PROMOTION.md +0 -74
- package/docs/DEPLOYMENT-CHECKS.md +0 -108
- package/docs/DEVELOPMENT-PIPELINE.md +0 -304
- package/docs/EGRESS.md +0 -125
- package/docs/EXTENSIONS.md +0 -403
- package/docs/FRAMEWORK.md +0 -217
- package/docs/FUNCTION-SECURITY.md +0 -251
- package/docs/HTTP.md +0 -129
- package/docs/INSTALL.md +0 -128
- package/docs/INTERCHANGE.md +0 -134
- package/docs/LOAD-TESTING.md +0 -91
- package/docs/LOCAL-DEVELOPMENT.md +0 -102
- package/docs/MIDDLEWARE-EXAMPLES.md +0 -75
- package/docs/MIDDLEWARE.md +0 -102
- package/docs/MONITORING.md +0 -115
- package/docs/OBSERVABILITY.md +0 -222
- package/docs/OPEN-DECISIONS.md +0 -149
- package/docs/OPERATIONAL-PROOF.md +0 -41
- package/docs/OPERATIONS.md +0 -201
- package/docs/ORGANIZATION.md +0 -135
- package/docs/PERFORMANCE.md +0 -72
- package/docs/PLUGINS.md +0 -271
- package/docs/POLICIES.md +0 -211
- package/docs/PRERENDER.md +0 -245
- package/docs/PROJECT-DIRECTION.md +0 -118
- package/docs/PROVIDER-VERIFICATION.md +0 -84
- package/docs/READINESS.md +0 -153
- package/docs/README.md +0 -93
- package/docs/RECIPES.md +0 -99
- package/docs/RELEASE-0.4.0-alpha.3.md +0 -50
- package/docs/RELEASE-0.4.1.md +0 -73
- package/docs/RELEASE-0.4.2.md +0 -30
- package/docs/RELEASE-READINESS.md +0 -146
- package/docs/RELEASE-SECURITY.md +0 -100
- package/docs/RESILIENCE.md +0 -161
- package/docs/ROUTING.md +0 -92
- package/docs/SANDBOX-REVIEW.md +0 -72
- package/docs/SCAFFOLDING.md +0 -70
- package/docs/SECURITY-AUDIT.md +0 -164
- package/docs/SITE.md +0 -150
- package/docs/SPECIFICATION.md +0 -359
- package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +0 -288
- package/docs/SPIKE-BUSINESS-SUITE.md +0 -1029
- package/docs/SPIKE-CORE-LAYERING.md +0 -368
- package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +0 -207
- package/docs/STANDARDS.md +0 -311
- package/docs/STARTERS.md +0 -83
- package/docs/STATIC.md +0 -105
- package/docs/TOOLING.md +0 -298
- package/docs/TUNNELS.md +0 -72
- package/docs/TYPESCRIPT-AUTHORING.md +0 -87
- package/docs/TYPESCRIPT.md +0 -123
- package/docs/VERCEL.md +0 -114
- package/docs/VERSION-ALIGNMENT.md +0 -88
- package/docs/YAML-GUIDE.md +0 -57
- package/docs/YAML-REFERENCE.md +0 -449
- package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +0 -68
- package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +0 -102
- package/docs/archive/2026-09-19/NEXT-PHASE-PLAN.md +0 -108
- package/docs/archive/2026-09-19/NEXT-STEPS.md +0 -646
- package/docs/archive/2026-09-19/OPEN-DECISIONS.md +0 -277
- package/docs/archive/2026-09-19/RELEASE-SECURITY.md +0 -186
- package/docs/archive/2026-09-19/ROADMAP.md +0 -387
- package/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md +0 -430
- package/docs/archive/2026-09-19/SPIKE-EXTENSIONS.md +0 -492
- package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +0 -365
- package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +0 -778
- package/docs/archive/2026-09-19/USABILITY-REVIEW.md +0 -139
- package/docs/archive/2026-09-20/OPEN-DECISIONS-COMPLETED.md +0 -116
- package/docs/archive/README.md +0 -29
- package/docs/policies/agents.md +0 -182
- package/docs/policies/cache.md +0 -152
- package/docs/policies/compression.md +0 -169
- package/docs/policies/contract.md +0 -52
- package/docs/policies/hardened.md +0 -56
- package/docs/policies/interoperability.md +0 -169
- package/docs/policies/operations.md +0 -45
- package/docs/policies/security.md +0 -161
- package/docs/policies/throttle.md +0 -103
- package/docs/yaml/assets.md +0 -36
- package/docs/yaml/conditions.md +0 -20
- package/docs/yaml/functions.md +0 -168
- package/docs/yaml/middleware.md +0 -31
- package/docs/yaml/organization.md +0 -74
- package/docs/yaml/policies.md +0 -37
- package/docs/yaml/redirects.md +0 -64
- package/docs/yaml/responses.md +0 -57
- package/docs/yaml/site.md +0 -24
- package/packaging/claude-plugin/.claude-plugin/plugin.json +0 -19
- package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +0 -130
- 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.
|