@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,492 +0,0 @@
|
|
|
1
|
-
# Historical record
|
|
2
|
-
|
|
3
|
-
Archived 2026-09-19. This records an earlier implementation or proposal, not
|
|
4
|
-
current instructions. See the [current roadmap](../../../ROADMAP.md),
|
|
5
|
-
[current contract](../../SPECIFICATION.md) and [open decisions](../../OPEN-DECISIONS.md).
|
|
6
|
-
Remaining acceptance work is not declared complete by archiving this record.
|
|
7
|
-
|
|
8
|
-
<!-- trust-model-prose: historical-file -->
|
|
9
|
-
<!-- guidance-claims: ignore-file -->
|
|
10
|
-
|
|
11
|
-
# Spike: plugins, adapters and optional runtime features
|
|
12
|
-
|
|
13
|
-
> Status update: the host hook seam, the plugin API and the five policies of
|
|
14
|
-
> section 5 are implemented; [policies](../../POLICIES.md) and [plugins](../../PLUGINS.md)
|
|
15
|
-
> describe the shipped behavior, which takes precedence where this text differs.
|
|
16
|
-
>
|
|
17
|
-
> Also stale: the embedding-API row below (`openLinkStore`, `startLinkApi`)
|
|
18
|
-
> and the `link` mentions in the gaps table describe core from before `link`
|
|
19
|
-
> was extracted. Core has no native `link`/`LinkStore` API any more, and the
|
|
20
|
-
> `urlcode-dynamic-link` package it was extracted into has since been retired,
|
|
21
|
-
> unpublished and deleted (September 2026).
|
|
22
|
-
|
|
23
|
-
Status: exploratory. Nothing here is committed scope; it records what the
|
|
24
|
-
runtime has today, how comparable tools expose the same needs, which gaps
|
|
25
|
-
matter for real deployments, and a proposed shape for closing them without
|
|
26
|
-
breaking the [project boundary](../../PROJECT-DIRECTION.md). The
|
|
27
|
-
[roadmap](ROADMAP.md) owns sequence; the [specification](../../SPECIFICATION.md)
|
|
28
|
-
owns what is implemented.
|
|
29
|
-
|
|
30
|
-
Every feature below is **optional and off by default**. A `version: "1"`
|
|
31
|
-
project with none of them keeps its current behavior byte for byte.
|
|
32
|
-
|
|
33
|
-
## 1. What exists today
|
|
34
|
-
|
|
35
|
-
### Extension surfaces
|
|
36
|
-
|
|
37
|
-
| Surface | Where | Who can use it | Runs where |
|
|
38
|
-
|---|---|---|---|
|
|
39
|
-
| Route middleware | `routes.<path>.middleware[]` | Project author | QuickJS/WASM guest, one chain per request, 16 max, route-local only |
|
|
40
|
-
| Functions | `routes.<path>.function` | Project author | Same guest, one per route |
|
|
41
|
-
| Provider adapters | `@jimhoyd/urlcode/vercel`, `@jimhoyd/urlcode/aws`, `@jimhoyd/urlcode/cloudflare` | Operator | Host process; wrap `runtime.handle()` |
|
|
42
|
-
| Build helpers | `@jimhoyd/urlcode/prerender` | Operator/build | Host process, build time only |
|
|
43
|
-
| Embedding API | `createRuntime`, `startServer`, `openLinkStore`, `startLinkApi` | Operator application | Host process |
|
|
44
|
-
| Operator policy | `URLCODE_POLICY` / policy file | Operator | Host, revision-pinned grants |
|
|
45
|
-
|
|
46
|
-
Two facts shape every proposal in this spike:
|
|
47
|
-
|
|
48
|
-
1. **There is no trusted, host-side, project-declared hook.** The only
|
|
49
|
-
project-level code path is guest middleware, which cannot see the socket,
|
|
50
|
-
client address, other requests or a clock beyond `Date.now()`. Anything that
|
|
51
|
-
needs cross-request state (rate counters, caches) or raw transport
|
|
52
|
-
(compression, connection budgets) cannot be middleware under the current
|
|
53
|
-
isolation model, and must not become one.
|
|
54
|
-
2. **There is no global middleware.** Every wrap is per route. Cross-cutting
|
|
55
|
-
behavior such as "block these agents everywhere" has to be repeated on
|
|
56
|
-
every route, and cannot cover native routes without dragging them into the
|
|
57
|
-
sandbox and losing their fast path.
|
|
58
|
-
|
|
59
|
-
### Request pipeline (self-hosted server)
|
|
60
|
-
|
|
61
|
-
```
|
|
62
|
-
socket limits (1024 conns, 15 s idle, 1000 req/socket)
|
|
63
|
-
→ in-flight admission (64 app, 16 health)
|
|
64
|
-
→ body read (bounded)
|
|
65
|
-
→ runtime.handle(): match → enabled/expires → methods → request.body policy
|
|
66
|
-
→ [guest chain: middleware… → handler] or native handler
|
|
67
|
-
→ response.headers overrides → writeResponse
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Adapters call `handle()` directly and skip the socket/admission stages,
|
|
71
|
-
delegating them to the provider.
|
|
72
|
-
|
|
73
|
-
### Already answered, in the operator's stack
|
|
74
|
-
|
|
75
|
-
The docs are consistent that these belong at ingress, not in route YAML:
|
|
76
|
-
rate limiting, WAF, TLS, DDoS mitigation, forwarded-header trust
|
|
77
|
-
([resilience](../../RESILIENCE.md), [capacity](../../CAPACITY.md)). Compression, CORS,
|
|
78
|
-
content negotiation and streaming are listed as explicitly outside the
|
|
79
|
-
[HTTP contract](../../HTTP.md). Caching is declarative and limited to a fixed
|
|
80
|
-
`cacheControl` vocabulary on asset handlers ([assets](../../ASSETS.md)).
|
|
81
|
-
|
|
82
|
-
## 2. How comparable tools do it
|
|
83
|
-
|
|
84
|
-
| Concern | Express / Koa | Fastify | Hono | Caddy | NGINX | Cloudflare / Vercel / Netlify | URLCode today |
|
|
85
|
-
|---|---|---|---|---|---|---|---|
|
|
86
|
-
| Plugin model | `app.use(fn)` global + router-local | `fastify.register(plugin, opts)` with encapsulation, `fastify-plugin` to break out; typed hooks (`onRequest`, `preHandler`, `onSend`…) | `app.use(path?, mw)`; first-party `hono/*` middleware | Modules compiled in; JSON/Caddyfile directives | Compiled modules; directives per `location` | Config file + edge middleware (`middleware.ts`, `_middleware`) | Route-local guest middleware only |
|
|
87
|
-
| Throttle | `express-rate-limit` (memory/Redis stores) | `@fastify/rate-limit` | `hono-rate-limiter` | `rate_limit` (plugin) | `limit_req`, `limit_conn` | Provider WAF/rate rules; Vercel Firewall; Netlify Rate Limiting | Global in-flight cap only; no per-client budget |
|
|
88
|
-
| Block bots / UA | `express-useragent`, `isbot` in middleware | `isbot` in hook | `isbot` in middleware | Request matchers on `header` | `map $http_user_agent`, `if`, `deny` | Bot Fight Mode, Super Bot Fight, WAF managed rules | None; would need per-route guest middleware, cannot cover native routes |
|
|
89
|
-
| Injection / hardening | `helmet`, `express-validator` | `@fastify/helmet`, schema validation built-in | `secureHeaders`, `validator` | Built-in header directives | Header directives | Managed WAF rulesets | Header injection prevented at runtime; body JSON syntax check; **no** security-header preset, no schema body validation |
|
|
90
|
-
| Compression | `compression` | `@fastify/compress` | `hono/compress` | `encode gzip zstd` | `gzip on; brotli` | Automatic at edge | None; identity only |
|
|
91
|
-
| Caching | `apicache`, CDN | `@fastify/caching` | `hono/cache` | `cache` (plugin) | `proxy_cache` | Edge cache + `Cache-Control`, ISR | Asset `cacheControl` vocabulary; no response cache |
|
|
92
|
-
| Templates | `res.render()`, view engines | `@fastify/view` | `hono/jsx`, `html` helper | `templates` directive | SSI | Framework-owned | None at request time; build-time [prerender](../../PRERENDER.md) only |
|
|
93
|
-
| Adapters | `serverless-http`, `@vendia`, `@hono/node-server` | `@fastify/aws-lambda` | First-party adapters for every runtime | n/a | n/a | n/a | Vercel, AWS, Cloudflare; native handlers only |
|
|
94
|
-
|
|
95
|
-
Three patterns recur and are worth borrowing:
|
|
96
|
-
|
|
97
|
-
- **Fastify's encapsulated `register` with typed lifecycle hooks.** Plugins
|
|
98
|
-
declare which phase they attach to. That gives a static, inspectable plugin
|
|
99
|
-
graph, which is exactly the property URLCode wants for a portable format.
|
|
100
|
-
- **Hono's "one first-party set of small middleware, each optional".** Small,
|
|
101
|
-
named, zero-config-by-default modules, every one of which is also expressible
|
|
102
|
-
on any adapter. That matches the "declarative first, code only when needed"
|
|
103
|
-
contract.
|
|
104
|
-
- **Caddy/NGINX declarative directives.** Behavior is data, not code, and can
|
|
105
|
-
be validated and exported. URLCode YAML is already this. The gap is that the
|
|
106
|
-
vocabulary stops at routes and headers.
|
|
107
|
-
|
|
108
|
-
## 3. Gap list
|
|
109
|
-
|
|
110
|
-
Ranked by how often a self-hosted operator hits it before the first deploy.
|
|
111
|
-
|
|
112
|
-
| # | Gap | Evidence | Severity |
|
|
113
|
-
|---|---|---|---|
|
|
114
|
-
| G1 | No cross-cutting (global) behavior | Middleware is route-local; every operator concern is copy-pasted per route or done at ingress | High |
|
|
115
|
-
| G2 | No per-client throttle | Only the 64/16 in-flight caps; shortener demo built its own | High |
|
|
116
|
-
| G3 | No bot / user-agent policy | Retrospectives, resilience doc: "filter upstream" | High for short-link use cases (crawlers hitting redirects) |
|
|
117
|
-
| G4 | No security-header preset | `helmet`-equivalent is the first thing every framework user adds | Medium |
|
|
118
|
-
| G5 | No compression | Assets and JSON go uncompressed unless a proxy handles it | Medium |
|
|
119
|
-
| G6 | No response cache / stale-while-revalidate | Functions are `no-store`; a hot function re-runs the sandbox per hit | Medium |
|
|
120
|
-
| G7 | No request-time template layer | Native bodies are opaque by design; prerender covers static sites but not dynamic pages | Medium |
|
|
121
|
-
| G8 | No host-side plugin contract | Adapters and `startServer` are the only host extension points and are undocumented as such | Medium |
|
|
122
|
-
| G9 | Adapters refuse functions/middleware/links | Documented and deliberate, but it means any feature built as guest middleware is also refused on serverless | Design constraint |
|
|
123
|
-
| G10 | No JSON Schema body validation | `request.body.format: json` checks syntax only | Low |
|
|
124
|
-
| G11 | No CORS preflight helper | Documented gap | Low |
|
|
125
|
-
| G12 | No SPA fallback for client-routed apps | [Assets](../../ASSETS.md) rules it out beside directory listing and trailing-slash redirects; a host plugin cannot supply it either, because an unmatched path throws 404 before the request object or any plugin hook exists | Low |
|
|
126
|
-
|
|
127
|
-
## 4. Design constraints these must respect
|
|
128
|
-
|
|
129
|
-
From [AGENTS.md](../../../AGENTS.md), [project direction](../../PROJECT-DIRECTION.md)
|
|
130
|
-
and [function security](../../FUNCTION-SECURITY.md):
|
|
131
|
-
|
|
132
|
-
- Route YAML describes **behavior**, not infrastructure. A throttle budget is
|
|
133
|
-
behavior ("this route allows 10 requests per minute per client"); a Redis
|
|
134
|
-
URL is infrastructure and stays in operator config.
|
|
135
|
-
- Guest code stays untrusted and capability-free. None of the features below
|
|
136
|
-
expose new host APIs to the sandbox.
|
|
137
|
-
- A project must run on a laptop, a container and an adapter. Every feature
|
|
138
|
-
needs a documented answer for each target: native, refused at activation,
|
|
139
|
-
or delegated to the provider with the same declared semantics.
|
|
140
|
-
- Unknown YAML fields fail. New vocabulary must land in the schema with
|
|
141
|
-
generated reference docs and executable examples.
|
|
142
|
-
|
|
143
|
-
## 5. Proposal: a `policies` block plus host-side plugins
|
|
144
|
-
|
|
145
|
-
Two additions, both optional. Declarative policies cover the common cases
|
|
146
|
-
without code; a host plugin contract covers the rest for operators who embed
|
|
147
|
-
the runtime.
|
|
148
|
-
|
|
149
|
-
The runtime stays generic: it ships mechanisms and named profiles, never an
|
|
150
|
-
opinion about who should be blocked or which vendor should sit in front of
|
|
151
|
-
the origin. Recommendations for a hardened deployment are collected in
|
|
152
|
-
section 6 as guidance an operator applies, not as defaults the runtime
|
|
153
|
-
imposes.
|
|
154
|
-
|
|
155
|
-
### 5.1 Portability rule
|
|
156
|
-
|
|
157
|
-
A project file is portable when a second person can run it on a different
|
|
158
|
-
host and get the same declared behavior or an explicit refusal. Every policy
|
|
159
|
-
therefore follows four rules:
|
|
160
|
-
|
|
161
|
-
1. **Vocabulary comes from a published standard** wherever one exists, so
|
|
162
|
-
the values are already documented outside this project and can be
|
|
163
|
-
translated to any proxy, CDN or framework.
|
|
164
|
-
2. **No operator identity in YAML.** Trusted proxy ranges, storage URLs,
|
|
165
|
-
list-refresh credentials and vendor rule identifiers live in operator
|
|
166
|
-
configuration (`urlcode serve` flags, environment, policy file), never in
|
|
167
|
-
the project.
|
|
168
|
-
3. **Enforce or refuse, never degrade silently.** Each policy has a
|
|
169
|
-
per-target row (self-hosted, Vercel/AWS, Cloudflare). A target that cannot
|
|
170
|
-
enforce a policy refuses activation naming the route and policy, exactly as
|
|
171
|
-
adapters already refuse functions. `urlcode audit` reports the table.
|
|
172
|
-
4. **Deterministic on identical input.** Given the same request bytes and the
|
|
173
|
-
same project, every target answers the same status and headers; only
|
|
174
|
-
cross-request state (throttle counters, cache hits) may differ, and that
|
|
175
|
-
difference is documented.
|
|
176
|
-
|
|
177
|
-
### 5.2 Declarative `policies`
|
|
178
|
-
|
|
179
|
-
```yaml
|
|
180
|
-
version: "1"
|
|
181
|
-
policies: # project defaults; each key optional
|
|
182
|
-
profile: hardened # named preset, see 6.1; explicit keys override
|
|
183
|
-
throttle:
|
|
184
|
-
quota: 60 # RateLimit-Policy: "default";q=60;w=60
|
|
185
|
-
window: 60
|
|
186
|
-
partition: client # client | route | client-route
|
|
187
|
-
status: 429
|
|
188
|
-
agents:
|
|
189
|
-
deny: [ai-crawlers] # named list bundled with the runtime, see 5.3
|
|
190
|
-
denyPatterns: ["^curl/"] # anchored, bounded, linear-time subset
|
|
191
|
-
allowPatterns: ["^Mozilla/5\\.0 \\(compatible; Googlebot"]
|
|
192
|
-
status: 403
|
|
193
|
-
security:
|
|
194
|
-
headers: oshp # oshp | oshp-no-csp | off
|
|
195
|
-
compression:
|
|
196
|
-
encodings: [br, gzip] # RFC 9110 content codings, preference order
|
|
197
|
-
minBytes: 1024
|
|
198
|
-
types: [text/*, application/json, application/javascript, image/svg+xml]
|
|
199
|
-
cache:
|
|
200
|
-
strategy: swr # see 5.4 catalogue
|
|
201
|
-
maxAge: 30
|
|
202
|
-
staleWhileRevalidate: 300
|
|
203
|
-
vary: [accept-language]
|
|
204
|
-
|
|
205
|
-
routes:
|
|
206
|
-
/api/lookup/{id}:
|
|
207
|
-
function: { source: functions/lookup.mjs }
|
|
208
|
-
policies:
|
|
209
|
-
throttle: { quota: 10, window: 60 }
|
|
210
|
-
cache: false
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
| Policy | Standard it is expressed in | Self-hosted | Vercel / AWS | Cloudflare build |
|
|
214
|
-
|---|---|---|---|---|
|
|
215
|
-
| throttle | RFC 6585 (429), RFC 9110 `Retry-After`, IETF httpapi `RateLimit`/`RateLimit-Policy` draft fields | native, in-process | refused unless `partition: route` (no shared state) | refused; guide maps quota/window to a provider rate rule |
|
|
216
|
-
| agents | RFC 9110 `User-Agent` product tokens; RFC 9309 for the companion `robots.txt`; bot-auth drafts for verified allow | native | native | compiled |
|
|
217
|
-
| security | OWASP Secure Headers Project values; CSP Level 3; RFC 6797 HSTS | native | native | compiled |
|
|
218
|
-
| compression | RFC 9110 `Accept-Encoding`/`Content-Encoding`; RFC 1952 gzip, RFC 7932 brotli, RFC 8878 zstd | native, assets precompressed at snapshot | refused: provider does it | refused: provider does it |
|
|
219
|
-
| cache | RFC 9111; RFC 5861 `stale-while-revalidate`/`stale-if-error`; RFC 8246 `immutable`; RFC 9213 `CDN-Cache-Control` | native origin cache plus headers | headers only | headers only |
|
|
220
|
-
|
|
221
|
-
**Throttle.** Values mirror the IETF `RateLimit-Policy` structured field
|
|
222
|
-
(`q` quota, `w` window in seconds) so the runtime can emit
|
|
223
|
-
`RateLimit-Policy` and `RateLimit` on every response and `Retry-After` on
|
|
224
|
-
429 without inventing a second vocabulary; an NGINX `limit_req` or a CDN
|
|
225
|
-
rule expresses the same numbers. Algorithm is a sliding-window counter,
|
|
226
|
-
the standard middle ground between fixed windows (burst at boundaries) and
|
|
227
|
-
token buckets (harder to explain in headers). `client` identity is the
|
|
228
|
-
socket peer unless `urlcode serve --trusted-proxies <cidr,...>` names the
|
|
229
|
-
proxies allowed to set `X-Forwarded-For`; only `X-Forwarded-For` is read;
|
|
230
|
-
RFC 7239 `Forwarded` is not parsed. Counters are in-process with a bounded LRU
|
|
231
|
-
table; multi-instance sharing is a host plugin concern (5.5).
|
|
232
|
-
|
|
233
|
-
**Agents.** Matching is against the `User-Agent` field only; product tokens
|
|
234
|
-
are compared case-insensitively. `deny` names a bundled list; patterns use
|
|
235
|
-
a linear-time subset (anchors, classes, alternation, bounded repetition,
|
|
236
|
-
no backreferences or lookaround, 256 bytes max) so a project cannot make
|
|
237
|
-
the matcher a ReDoS vector. `allowPatterns` win over `deny`, which lets an
|
|
238
|
-
operator keep a search crawler while denying a category. The runtime
|
|
239
|
-
matches strings only; verifying that a claimed agent is genuine (reverse
|
|
240
|
-
DNS as documented by the major search engines, or the HTTP Message
|
|
241
|
-
Signature based web-bot-auth drafts) is a plugin concern. Denials answer
|
|
242
|
-
with the configured status and an empty body and log the list name, never
|
|
243
|
-
the raw header. A `robots.txt` route remains an ordinary `respond` or
|
|
244
|
-
`page` handler; `urlcode init` can generate one from the same lists.
|
|
245
|
-
|
|
246
|
-
**Security headers.** Profiles copy the current OWASP Secure Headers
|
|
247
|
-
Project recommended values verbatim and record the OSHP revision in the
|
|
248
|
-
generated reference, so "what does `oshp` set" is answerable from a public
|
|
249
|
-
source. Explicit `response.headers` override profile values header by
|
|
250
|
-
header. HSTS is only emitted when `--origin` is `https`.
|
|
251
|
-
|
|
252
|
-
**Compression.** Negotiation follows RFC 9110 `Accept-Encoding` q-values
|
|
253
|
-
with the project's `encodings` order as tie-breaker. Skipped when the
|
|
254
|
-
response already carries `Content-Encoding`, is `206`, carries
|
|
255
|
-
`Cache-Control: no-transform`, or is below `minBytes`. Asset snapshots are
|
|
256
|
-
precompressed at load, the same trick as NGINX `gzip_static` and Caddy
|
|
257
|
-
`precompressed`, so a request costs a buffer copy. The runtime adds
|
|
258
|
-
`Vary: Accept-Encoding`. When a response carries a session cookie or a
|
|
259
|
-
route declares `secrets`, compression is skipped (BREACH mitigation) unless
|
|
260
|
-
the route says `compression: { allowWithSecrets: true }`.
|
|
261
|
-
|
|
262
|
-
### 5.3 Bot lists: which to bundle and how to refresh
|
|
263
|
-
|
|
264
|
-
Vendoring a list into an Apache-2.0 package requires a license that allows
|
|
265
|
-
redistribution with attribution and no share-alike obligation. Findings:
|
|
266
|
-
|
|
267
|
-
| Source | License | Format | Maintenance | Bundle? |
|
|
268
|
-
|---|---|---|---|---|
|
|
269
|
-
| [ai-robots-txt/ai.robots.txt](https://github.com/ai-robots-txt/ai.robots.txt) | MIT | `robots.json` plus generated `robots.txt`, NGINX, Caddy, HAProxy, Apache files | Tagged releases, Atom feed, GitHub Action regenerates outputs from JSON | **Yes**: `ai-crawlers` |
|
|
270
|
-
| [monperrus/crawler-user-agents](https://github.com/monperrus/crawler-user-agents) | MIT (CC-SA before 2016-11-07; use only later revisions) | JSON with `pattern`, `url`, `instances`, `tags` | npm/PyPI/Go packages, PR-driven | **Yes**: `crawlers`, tags give `seo`, `monitoring` sub-lists |
|
|
271
|
-
| [atmire/COUNTER-Robots](https://github.com/atmire/COUNTER-Robots) | MIT | JSON with `pattern`, dates, generated plain text | Library-statistics community, periodic | Optional: `counter-robots` for analytics-exclusion use |
|
|
272
|
-
| [omrilotan/isbot](https://github.com/omrilotan/isbot) | Unlicense (public domain) | Aggregated regex parts exported as `list` | npm releases; aggregates the two above plus device-detector and vendor lists | Not directly: it includes LGPL-derived device-detector data, so vendor its upstream sources instead |
|
|
273
|
-
| matomo device-detector | LGPL-3.0 | YAML regexes | Active | **No**: copyleft data, not vendored |
|
|
274
|
-
| Cloudflare / Google / Bing verified-bot data | Proprietary or API-only | Reverse-DNS and IP ranges | Vendor | **No**: verification belongs in a plugin that calls the vendor |
|
|
275
|
-
| IAB/ABC spiders list | Paid, proprietary | Text | Commercial | **No** |
|
|
276
|
-
|
|
277
|
-
Bundling plan:
|
|
278
|
-
|
|
279
|
-
- Ship `data/agents/<list>.json` normalized to one schema
|
|
280
|
-
(`{name, pattern, source, sourceRevision, addedAt}`), with each upstream
|
|
281
|
-
`LICENSE` reproduced under `data/agents/LICENSES/` and named in `NOTICE`
|
|
282
|
-
as Apache-2.0 §4(d) requires.
|
|
283
|
-
- A `scripts/sync-agent-lists.ts` pulls pinned upstream tags, validates every
|
|
284
|
-
pattern against the linear-time subset (rejecting or rewriting the rest),
|
|
285
|
-
and records the upstream revision. Refresh is a normal pull request run by
|
|
286
|
-
Dependabot-style automation on a schedule; a release notes the list
|
|
287
|
-
revisions it carries, and `urlcode doctor` prints them.
|
|
288
|
-
- Projects may also point at their own file (`deny: [./agents/deny.json]`)
|
|
289
|
-
in the same schema, which keeps the YAML portable while letting an
|
|
290
|
-
operator use a list the project does not want to redistribute.
|
|
291
|
-
|
|
292
|
-
### 5.4 Caching strategies catalogue
|
|
293
|
-
|
|
294
|
-
Caching is where "just one setting" fails users most. The policy therefore
|
|
295
|
-
names a strategy from a fixed catalogue; each row is a standard pattern
|
|
296
|
-
with a known name outside this project and a defined header output, so a
|
|
297
|
-
CDN or proxy in front of the origin interprets the result correctly.
|
|
298
|
-
|
|
299
|
-
| `strategy` | Emitted headers | Origin memory cache | Typical use |
|
|
300
|
-
|---|---|---|---|
|
|
301
|
-
| `no-store` (current function/redirect default) | `Cache-Control: no-store` | off | personalized, secret-bearing |
|
|
302
|
-
| `revalidate` (current asset default) | `Cache-Control: no-cache`, `ETag`, `Last-Modified`; answers `304` to `If-None-Match`/`If-Modified-Since` | off | HTML, anything that must be fresh but is cheap to validate |
|
|
303
|
-
| `public` | `Cache-Control: public, max-age=N` | optional | stable API answers, feeds |
|
|
304
|
-
| `immutable` | `Cache-Control: public, max-age=31536000, immutable` (RFC 8246) | off | content-hashed asset URLs only; the runtime refuses it on unhashed paths unless `force: true` |
|
|
305
|
-
| `swr` | `Cache-Control: public, max-age=N, stale-while-revalidate=M` (RFC 5861) | on: serves stale and refreshes once in the background | hot functions, link previews |
|
|
306
|
-
| `sie` | adds `stale-if-error=M` (RFC 5861) | on | keep answering during an upstream failure |
|
|
307
|
-
| `micro` | `Cache-Control: no-store` to clients; origin cache TTL of 1 to 5 seconds | on | the NGINX micro-caching pattern: absorb thundering herds on a function without changing client semantics |
|
|
308
|
-
| `cdn-only` | `Cache-Control: no-store` plus `CDN-Cache-Control: max-age=N` (RFC 9213) | off | let the CDN cache while browsers do not |
|
|
309
|
-
| `private` | `Cache-Control: private, max-age=N` | off | per-user data that a browser may keep |
|
|
310
|
-
|
|
311
|
-
Rules common to all strategies:
|
|
312
|
-
|
|
313
|
-
- Only `GET`/`HEAD` and status 200, 301, 302, 404, 410 enter the origin
|
|
314
|
-
cache. Responses carrying `Set-Cookie`, routes declaring `secrets`, and
|
|
315
|
-
responses with `Cache-Control: private` or `no-store` are never stored.
|
|
316
|
-
- Cache key is method, path, query and the declared `vary` headers, and
|
|
317
|
-
the runtime emits a matching `Vary`. `Accept-Encoding` is added
|
|
318
|
-
automatically when compression is on.
|
|
319
|
-
- The origin cache is keyed by snapshot version and dropped on reload, so a
|
|
320
|
-
deploy never serves the previous code's output.
|
|
321
|
-
- Concurrent misses for one key coalesce into one handler invocation
|
|
322
|
-
(`singleflight`, NGINX `proxy_cache_lock`), which is the actual reason
|
|
323
|
-
to cache a function at all.
|
|
324
|
-
- Memory bound and hit/miss/stale counters are exposed through the health
|
|
325
|
-
endpoint and request logs.
|
|
326
|
-
- Surrogate keys and purge (`Surrogate-Key`, `Cache-Tag`) are out of scope
|
|
327
|
-
for the runtime; a plugin can add them.
|
|
328
|
-
|
|
329
|
-
### 5.5 Host-side plugin contract (`@jimhoyd/urlcode/plugins`)
|
|
330
|
-
|
|
331
|
-
For operators embedding the runtime who need behavior the declarative block
|
|
332
|
-
cannot express, add a small, documented, host-trusted hook API modelled on
|
|
333
|
-
Fastify's phases. Plugins are **not** part of the project format; they are
|
|
334
|
-
passed by the operator application to `startServer`/`createRuntime`, so a
|
|
335
|
-
project stays portable while an operator can still add a shared-store
|
|
336
|
-
limiter, a verified-bot check or a cache purge endpoint.
|
|
337
|
-
|
|
338
|
-
```js
|
|
339
|
-
import { startServer } from '@jimhoyd/urlcode';
|
|
340
|
-
|
|
341
|
-
await startServer({
|
|
342
|
-
project: './site',
|
|
343
|
-
plugins: [
|
|
344
|
-
{
|
|
345
|
-
name: 'shared-throttle', version: '1.0.0', targets: ['node'],
|
|
346
|
-
onActivate(runtime) { /* inspect runtime.testPlan(); throw to refuse */ },
|
|
347
|
-
onRequest(ctx) { /* ctx.method, path, headers, client; return a result to short-circuit */ },
|
|
348
|
-
onResponse(ctx, result) { /* return a replaced result; body stays a Buffer */ },
|
|
349
|
-
onError(ctx, error) {},
|
|
350
|
-
onClose() {},
|
|
351
|
-
},
|
|
352
|
-
],
|
|
353
|
-
});
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
Rules:
|
|
357
|
-
|
|
358
|
-
- No hook can reach inside the guest, extend a deadline, or see bindings.
|
|
359
|
-
- Plugins declare `name`, `version` and `targets`; activation refuses a
|
|
360
|
-
plugin on a host it does not list, mirroring route refusal.
|
|
361
|
-
- The declarative `policies` are implemented on this same interface, so
|
|
362
|
-
first-party and third-party behavior share one code path and one test
|
|
363
|
-
harness.
|
|
364
|
-
- Plugins are host code and therefore the operator's trust boundary, not
|
|
365
|
-
the project's. The documentation must say so plainly.
|
|
366
|
-
|
|
367
|
-
### 5.6 Templates
|
|
368
|
-
|
|
369
|
-
Request-time templating conflicts with the opaque-native-body rule, and the
|
|
370
|
-
[prerender](../../PRERENDER.md) helper already handles the static case. Two
|
|
371
|
-
bounded options fit the boundary:
|
|
372
|
-
|
|
373
|
-
1. **Build-time only (recommended first).** Promote prerender into a CLI
|
|
374
|
-
command, `urlcode build --prerender`, and add a `layouts` convention in
|
|
375
|
-
the starter so a page function can import a layout module and the output
|
|
376
|
-
is native. Zero runtime change, works on every target.
|
|
377
|
-
2. **Declarative `page.template` (later, if demanded).** A `page` handler
|
|
378
|
-
may name a template file plus a `slots` map of literal strings or
|
|
379
|
-
validated inputs. Rendering is pure substitution with contextual HTML
|
|
380
|
-
escaping, done at snapshot time for literal slots and on the request path
|
|
381
|
-
only for input-driven slots. No expressions, no loops, no guest code.
|
|
382
|
-
Closer to server-side includes than to a view engine, and that is the
|
|
383
|
-
point: inspectable and portable.
|
|
384
|
-
|
|
385
|
-
A general view engine is out of scope: it would be a second code path with
|
|
386
|
-
its own sandbox questions.
|
|
387
|
-
|
|
388
|
-
## 6. Hardened configuration guidance
|
|
389
|
-
|
|
390
|
-
This section is advice, not defaults. It reflects patterns that have held
|
|
391
|
-
up under public traffic in the frameworks and proxies surveyed in section 2.
|
|
392
|
-
|
|
393
|
-
### 6.1 The `hardened` profile
|
|
394
|
-
|
|
395
|
-
`policies.profile: hardened` expands to the following and nothing else, so
|
|
396
|
-
it can be read in one place and overridden key by key:
|
|
397
|
-
|
|
398
|
-
```yaml
|
|
399
|
-
policies:
|
|
400
|
-
security: { headers: oshp }
|
|
401
|
-
agents: { deny: [ai-crawlers], status: 403 }
|
|
402
|
-
throttle: { quota: 120, window: 60, partition: client, status: 429 }
|
|
403
|
-
compression: { encodings: [br, gzip], minBytes: 1024 }
|
|
404
|
-
cache: { strategy: revalidate }
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
The numbers are starting points chosen to be safe for a single small
|
|
408
|
-
instance, not tuned for any workload. A `strict` profile is deliberately
|
|
409
|
-
not offered: anything stricter is a per-project decision.
|
|
410
|
-
|
|
411
|
-
### 6.2 Layering, in order of where a request is stopped
|
|
412
|
-
|
|
413
|
-
1. **Network and edge.** Volumetric protection, TLS termination and
|
|
414
|
-
per-client connection budgets stay with the hosting provider or the
|
|
415
|
-
reverse proxy, as [resilience](../../RESILIENCE.md) already states. Runtime
|
|
416
|
-
policies are a second layer, never the first.
|
|
417
|
-
2. **Ingress to origin.** Bind privately; allow only the proxy's addresses;
|
|
418
|
-
pass `--trusted-proxies` so `client` partitioning uses the real peer.
|
|
419
|
-
Never trust `X-Forwarded-For` from an untrusted hop.
|
|
420
|
-
3. **Runtime request policies.** Agents before throttle (denials are
|
|
421
|
-
cheaper than counting), then admission, then routing.
|
|
422
|
-
4. **Route contract.** Exact methods, `request.body` limits and content
|
|
423
|
-
types, `expires` on campaign routes.
|
|
424
|
-
5. **Response policies.** Security headers on everything, compression only
|
|
425
|
-
on listed types, caching only on the strategies whose semantics you can
|
|
426
|
-
state, `no-store` everywhere else.
|
|
427
|
-
|
|
428
|
-
### 6.3 Strategies that have proven out elsewhere
|
|
429
|
-
|
|
430
|
-
- **Emit rate-limit headers even before enforcing.** Running throttle in
|
|
431
|
-
`report` mode (headers plus a log line, no 429) for a release before
|
|
432
|
-
turning on enforcement is how most API operators find their real quotas.
|
|
433
|
-
Proposed: `throttle.mode: enforce | report`.
|
|
434
|
-
- **Deny lists as data with a pinned revision.** Every proxy that blocks
|
|
435
|
-
agents well treats the list as a versioned artifact that ships with the
|
|
436
|
-
deploy, not a live feed, so a rollback also rolls back the list.
|
|
437
|
-
- **Allow before deny.** Keep an explicit allow for the crawlers you need
|
|
438
|
-
indexed; broad denies without it are the most common self-inflicted
|
|
439
|
-
outage in this space.
|
|
440
|
-
- **Micro-cache the expensive path, revalidate the rest.** One-second
|
|
441
|
-
origin caching on a hot function removes most thundering-herd load
|
|
442
|
-
without changing what a browser sees.
|
|
443
|
-
- **Immutable only with content hashes.** Long `max-age` on a path that can
|
|
444
|
-
change is the classic stale-asset bug; the runtime refusing `immutable`
|
|
445
|
-
on unhashed paths encodes that lesson.
|
|
446
|
-
- **Compression off on secret-bearing responses.** Compression plus
|
|
447
|
-
attacker-controlled input in the same response is the BREACH class of
|
|
448
|
-
attack; skip it where secrets or session cookies are present.
|
|
449
|
-
- **Report the capability table.** Print which policies are enforced, which
|
|
450
|
-
are refused and which are delegated on the current target at startup and
|
|
451
|
-
in `urlcode audit`, so a person who copies the YAML to another host sees
|
|
452
|
-
the difference immediately.
|
|
453
|
-
|
|
454
|
-
## 7. What this spike does not recommend
|
|
455
|
-
|
|
456
|
-
- **Global guest middleware.** It would pull every native route into the
|
|
457
|
-
sandbox and end the fast path. Cross-cutting behavior belongs on the host.
|
|
458
|
-
- **A plugin field in route YAML that names npm packages.** It breaks the
|
|
459
|
-
"project is portable, operator owns trust" split.
|
|
460
|
-
- **Provider settings in YAML** such as vendor rule identifiers. Declared
|
|
461
|
-
semantics map to provider features in the adapter, not in the project.
|
|
462
|
-
- **Shared-state throttling in the runtime.** Multi-instance coordination is
|
|
463
|
-
a plugin's job.
|
|
464
|
-
- **Copyleft or proprietary agent data.** Only MIT/public-domain lists are
|
|
465
|
-
vendored; verification against vendor systems stays in plugins.
|
|
466
|
-
|
|
467
|
-
## 8. Suggested sequence
|
|
468
|
-
|
|
469
|
-
| Step | Scope | Why first |
|
|
470
|
-
|---|---|---|
|
|
471
|
-
| 1 | Internal host hook interface + `policies.security` | Smallest change; establishes the plugin seam with pure header math that works on every target |
|
|
472
|
-
| 2 | `policies.agents` with bundled lists, sync script, NOTICE entries | High demand for short-link projects; stateless; compiles to Cloudflare |
|
|
473
|
-
| 3 | `policies.throttle` with `report` mode, RateLimit headers and `--trusted-proxies` | Removes the most repeated application-layer code; reuses existing admission counters |
|
|
474
|
-
| 4 | `policies.compression` with precompressed asset snapshots | Measurable win in benchmarks |
|
|
475
|
-
| 5 | `policies.cache` catalogue | Depends on clear rules from steps 1 to 4 for what is cacheable |
|
|
476
|
-
| 6 | Public `@jimhoyd/urlcode/plugins` API + one reference package (shared-store throttle) | Proves the seam from outside the repo |
|
|
477
|
-
| 7 | `urlcode build --prerender` and starter layouts | Template story without a runtime change |
|
|
478
|
-
|
|
479
|
-
Each step ships with schema changes, `npm run docs:reference`, cookbook
|
|
480
|
-
routes, capability-table updates and fixtures that assert the self-hosted
|
|
481
|
-
server and each adapter agree on status, body and headers.
|
|
482
|
-
|
|
483
|
-
## 9. Open questions
|
|
484
|
-
|
|
485
|
-
- Should `policies` be a top-level key or nested under a `server` key so
|
|
486
|
-
project-level defaults are visibly separate from routes?
|
|
487
|
-
- Does a denied agent count against the throttle? (Proposed: no.)
|
|
488
|
-
- The IETF `RateLimit` header fields and the web-bot-auth architecture are
|
|
489
|
-
still Internet-Drafts; the YAML keys are chosen to survive renames in the
|
|
490
|
-
header syntax, but the emitted field names may need a version switch.
|
|
491
|
-
- Which OSHP revision to pin first, and whether the CSP in `oshp` should be
|
|
492
|
-
report-only by default for `page` routes that carry inline scripts.
|