@jimhoyd/urlcode 0.3.0
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/CONTRIBUTING.md +80 -0
- package/LICENSE +202 -0
- package/NOTICE +20 -0
- package/README.md +342 -0
- package/ROADMAP.md +248 -0
- package/SECURITY.md +40 -0
- package/data/agents/LICENSES/ai-robots-txt.txt +24 -0
- package/data/agents/LICENSES/crawler-user-agents.txt +23 -0
- package/data/agents/ai-crawlers.json +1240 -0
- package/data/agents/crawlers.json +10515 -0
- package/data/agents/index.d.ts +3 -0
- package/data/agents/index.js +2453 -0
- package/data/agents/monitoring.json +1520 -0
- package/data/agents/seo.json +3865 -0
- package/dist/BUILD-MANIFEST.json +60 -0
- package/dist/adapters.js +59 -0
- package/dist/agent-lists.js +59 -0
- package/dist/assets.js +129 -0
- package/dist/authoring.js +68 -0
- package/dist/aws.js +110 -0
- package/dist/build-cloudflare.js +164 -0
- package/dist/cli.js +185 -0
- package/dist/client-address.js +85 -0
- package/dist/cloudflare.js +165 -0
- package/dist/compliance-rules/baseline.js +98 -0
- package/dist/compliance-rules/privacy.js +38 -0
- package/dist/compliance-rules/shared.js +62 -0
- package/dist/compliance-rules/strict.js +61 -0
- package/dist/compliance.js +219 -0
- package/dist/config-worker.js +9 -0
- package/dist/config.js +172 -0
- package/dist/errors.js +8 -0
- package/dist/function-sources.js +54 -0
- package/dist/function-worker.js +105 -0
- package/dist/functions.js +174 -0
- package/dist/guest-api.js +114 -0
- package/dist/header-validation.js +18 -0
- package/dist/http-policy.js +74 -0
- package/dist/http-response.js +84 -0
- package/dist/index.js +11 -0
- package/dist/link-api.js +136 -0
- package/dist/link-cli.js +141 -0
- package/dist/link-events.js +76 -0
- package/dist/link-records.js +31 -0
- package/dist/link-store-worker.js +150 -0
- package/dist/link-store.js +250 -0
- package/dist/logging.js +22 -0
- package/dist/management-policy.js +41 -0
- package/dist/match.js +124 -0
- package/dist/observability.js +242 -0
- package/dist/plugins.js +74 -0
- package/dist/policies/agents.js +248 -0
- package/dist/policies/cache.js +297 -0
- package/dist/policies/compression.js +187 -0
- package/dist/policies/security.js +0 -0
- package/dist/policies/throttle.js +131 -0
- package/dist/policies.js +142 -0
- package/dist/policy.js +55 -0
- package/dist/prerender.js +165 -0
- package/dist/project-tests.js +23 -0
- package/dist/readiness.js +216 -0
- package/dist/router.js +166 -0
- package/dist/runtime.js +244 -0
- package/dist/scaffold.js +0 -0
- package/dist/scripts/operational-drills.js +75 -0
- package/dist/server.js +270 -0
- package/dist/site.js +0 -0
- package/dist/sqlite-version.js +6 -0
- package/dist/types/adapters.d.ts +12 -0
- package/dist/types/agent-lists.d.ts +5 -0
- package/dist/types/assets.d.ts +13 -0
- package/dist/types/authoring.d.ts +2 -0
- package/dist/types/aws.d.ts +35 -0
- package/dist/types/build-cloudflare.d.ts +14 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/client-address.d.ts +9 -0
- package/dist/types/cloudflare.d.ts +75 -0
- package/dist/types/compliance-rules/baseline.d.ts +12 -0
- package/dist/types/compliance-rules/privacy.d.ts +6 -0
- package/dist/types/compliance-rules/shared.d.ts +24 -0
- package/dist/types/compliance-rules/strict.d.ts +9 -0
- package/dist/types/compliance.d.ts +119 -0
- package/dist/types/config-worker.d.ts +1 -0
- package/dist/types/config.d.ts +21 -0
- package/dist/types/errors.d.ts +7 -0
- package/dist/types/function-sources.d.ts +21 -0
- package/dist/types/function-worker.d.ts +1 -0
- package/dist/types/functions.d.ts +92 -0
- package/dist/types/guest-api.d.ts +16 -0
- package/dist/types/header-validation.d.ts +2 -0
- package/dist/types/http-policy.d.ts +39 -0
- package/dist/types/http-response.d.ts +43 -0
- package/dist/types/index.d.ts +11 -0
- package/dist/types/link-api.d.ts +30 -0
- package/dist/types/link-cli.d.ts +36 -0
- package/dist/types/link-events.d.ts +27 -0
- package/dist/types/link-records.d.ts +11 -0
- package/dist/types/link-store-worker.d.ts +1 -0
- package/dist/types/link-store.d.ts +130 -0
- package/dist/types/logging.d.ts +9 -0
- package/dist/types/management-policy.d.ts +9 -0
- package/dist/types/match.d.ts +81 -0
- package/dist/types/observability.d.ts +104 -0
- package/dist/types/plugins.d.ts +27 -0
- package/dist/types/policies/agents.d.ts +86 -0
- package/dist/types/policies/cache.d.ts +82 -0
- package/dist/types/policies/compression.d.ts +41 -0
- package/dist/types/policies/security.d.ts +37 -0
- package/dist/types/policies/throttle.d.ts +55 -0
- package/dist/types/policies.d.ts +36 -0
- package/dist/types/policy.d.ts +21 -0
- package/dist/types/prerender.d.ts +31 -0
- package/dist/types/project-tests.d.ts +13 -0
- package/dist/types/readiness.d.ts +121 -0
- package/dist/types/router.d.ts +11 -0
- package/dist/types/runtime.d.ts +91 -0
- package/dist/types/scaffold.d.ts +17 -0
- package/dist/types/server.d.ts +37 -0
- package/dist/types/site.d.ts +12 -0
- package/dist/types/sqlite-version.d.ts +1 -0
- package/dist/types/types.d.ts +332 -0
- package/dist/types/vercel.d.ts +12 -0
- package/dist/types.js +177 -0
- package/dist/vercel.js +65 -0
- package/docs/AI-AUTHORING.md +113 -0
- package/docs/ASSETS.md +106 -0
- package/docs/AWS.md +77 -0
- package/docs/BEST-PRACTICES.md +267 -0
- package/docs/CAPACITY.md +208 -0
- package/docs/CLOUDFLARE.md +110 -0
- package/docs/COMPLIANCE.md +242 -0
- package/docs/DYNAMIC-LINKS.md +561 -0
- package/docs/FUNCTION-SECURITY.md +113 -0
- package/docs/HTTP.md +129 -0
- package/docs/INSTALL.md +90 -0
- package/docs/LOAD-TESTING.md +91 -0
- package/docs/LOCAL-DEVELOPMENT.md +99 -0
- package/docs/MANAGEMENT-SECURITY.md +82 -0
- package/docs/MIDDLEWARE.md +83 -0
- package/docs/MONITORING.md +132 -0
- package/docs/OBSERVABILITY.md +229 -0
- package/docs/OPERATIONAL-PROOF.md +42 -0
- package/docs/OPERATIONS.md +207 -0
- package/docs/ORGANIZATION.md +135 -0
- package/docs/PERFORMANCE.md +72 -0
- package/docs/PLUGINS.md +234 -0
- package/docs/POLICIES.md +489 -0
- package/docs/PRERENDER.md +193 -0
- package/docs/PROJECT-DIRECTION.md +51 -0
- package/docs/READINESS.md +125 -0
- package/docs/README.md +58 -0
- package/docs/RELEASE-READINESS.md +95 -0
- package/docs/RELEASE-SECURITY.md +72 -0
- package/docs/RESILIENCE.md +160 -0
- package/docs/ROUTING.md +95 -0
- package/docs/SANDBOX-REVIEW.md +59 -0
- package/docs/SCAFFOLDING.md +72 -0
- package/docs/SECURITY-AUDIT.md +124 -0
- package/docs/SITE.md +150 -0
- package/docs/SPECIFICATION.md +232 -0
- package/docs/SPIKE-EXTENSIONS.md +475 -0
- package/docs/STANDARDS.md +303 -0
- package/docs/STARTERS.md +51 -0
- package/docs/TUNNELS.md +75 -0
- package/docs/TYPESCRIPT.md +102 -0
- package/docs/VERCEL.md +107 -0
- package/docs/YAML-GUIDE.md +521 -0
- package/docs/YAML-REFERENCE.md +328 -0
- package/docs/policies/agents.md +182 -0
- package/docs/policies/cache.md +139 -0
- package/docs/policies/compression.md +168 -0
- package/docs/policies/security.md +160 -0
- package/docs/policies/throttle.md +103 -0
- package/examples/assets/.env.example +1 -0
- package/examples/assets/.gitattributes +2 -0
- package/examples/assets/Makefile +30 -0
- package/examples/assets/README.md +18 -0
- package/examples/assets/functions/hello.mjs +3 -0
- package/examples/assets/gitignore.template +7 -0
- package/examples/assets/public/about.html +2 -0
- package/examples/assets/public/assets/example.txt +1 -0
- package/examples/assets/public/guide.txt +1 -0
- package/examples/assets/tests/requests.json +44 -0
- package/examples/assets/urlcode.yaml +27 -0
- package/examples/aws/README.md +13 -0
- package/examples/aws/handler.mjs +5 -0
- package/examples/aws/public/index.html +3 -0
- package/examples/aws/public/notes.txt +1 -0
- package/examples/aws/template.yaml +33 -0
- package/examples/aws/tests/requests.json +9 -0
- package/examples/aws/urlcode.yaml +19 -0
- package/examples/cloudflare/README.md +18 -0
- package/examples/cloudflare/package.json +14 -0
- package/examples/cloudflare/tests/requests.json +9 -0
- package/examples/cloudflare/urlcode.yaml +24 -0
- package/examples/cloudflare/wrangler.toml +5 -0
- package/examples/compliance/README.md +20 -0
- package/examples/compliance/rules.mjs +65 -0
- package/examples/cookbook/README.md +21 -0
- package/examples/cookbook/functions/choice.mjs +4 -0
- package/examples/cookbook/functions/echo.mjs +3 -0
- package/examples/cookbook/functions/hello.mjs +3 -0
- package/examples/cookbook/functions/text.mjs +3 -0
- package/examples/cookbook/middleware/headers.mjs +6 -0
- package/examples/cookbook/public/about.html +1 -0
- package/examples/cookbook/public/assets/index.html +1 -0
- package/examples/cookbook/public/assets/site.css +1 -0
- package/examples/cookbook/public/favicon.svg +1 -0
- package/examples/cookbook/public/guide.txt +1 -0
- package/examples/cookbook/public/llms.txt +6 -0
- package/examples/cookbook/routes/code.yaml +55 -0
- package/examples/cookbook/routes/files.yaml +17 -0
- package/examples/cookbook/routes/policies.yaml +28 -0
- package/examples/cookbook/routes/redirects.yaml +38 -0
- package/examples/cookbook/routes/responses.yaml +26 -0
- package/examples/cookbook/tests/requests.json +254 -0
- package/examples/cookbook/urlcode.yaml +22 -0
- package/examples/live-links/README.md +11 -0
- package/examples/live-links/tests/requests.json +6 -0
- package/examples/live-links/urlcode.yaml +16 -0
- package/examples/monitoring/blackbox-jobs.yaml +37 -0
- package/examples/monitoring/prometheus-rules.yaml +88 -0
- package/examples/monitoring/prometheus-scrape.yaml +20 -0
- package/examples/monitoring/vector.toml +65 -0
- package/examples/prerender/README.md +48 -0
- package/examples/prerender/functions/page.mjs +6 -0
- package/examples/prerender/middleware/template.mjs +29 -0
- package/examples/prerender/prerender.d.mts +4 -0
- package/examples/prerender/prerender.mjs +63 -0
- package/examples/prerender/tests/requests.json +39 -0
- package/examples/prerender/urlcode.yaml +50 -0
- package/examples/tunnel/dev-with-ngrok.sh +61 -0
- package/examples/vercel/README.md +13 -0
- package/examples/vercel/api/index.js +5 -0
- package/examples/vercel/package.json +8 -0
- package/examples/vercel/public/index.html +3 -0
- package/examples/vercel/public/notes.txt +1 -0
- package/examples/vercel/tests/requests.json +48 -0
- package/examples/vercel/urlcode.yaml +19 -0
- package/examples/vercel/vercel.json +10 -0
- package/llms.txt +46 -0
- package/package.json +123 -0
- package/schemas/urlcode.schema.json +1230 -0
- package/starters/default/.gitattributes +1 -0
- package/starters/default/Makefile +30 -0
- package/starters/default/README.md +34 -0
- package/starters/default/functions/hello.mjs +3 -0
- package/starters/default/gitignore.template +8 -0
- package/starters/default/middleware/headers.mjs +6 -0
- package/starters/default/routes/functions.yaml +20 -0
- package/starters/default/routes/marketing/links.yaml +7 -0
- package/starters/default/starter.json +5 -0
- package/starters/default/tests/requests.json +56 -0
- package/starters/default/urlcode.yaml +8 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Running URLCode yourself
|
|
2
|
+
|
|
3
|
+
This is the stable 0.1 self-hosted runtime. Its deliberately bounded feature set
|
|
4
|
+
is not a claim of suitability for every production workload. Deploy only workloads
|
|
5
|
+
whose requirements fit the [implemented contract](SPECIFICATION.md).
|
|
6
|
+
Provider adapters, automatic TLS/DNS management, distributed rate limits,
|
|
7
|
+
metrics exporters and durable event delivery are not included.
|
|
8
|
+
|
|
9
|
+
## Process deployment
|
|
10
|
+
|
|
11
|
+
Install a reviewed URLCode commit with Node 22.13+ and `npm ci --omit=dev`.
|
|
12
|
+
Keep the runtime separate from an application checkout pinned to its own commit.
|
|
13
|
+
Functions support only relative project JavaScript modules; do not install or
|
|
14
|
+
execute an untrusted application’s package scripts as part of serving it. Validate using
|
|
15
|
+
the same injected environment as the serving process:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
node /opt/urlcode/dist/cli.js validate --project /srv/my-links
|
|
19
|
+
node /opt/urlcode/dist/cli.js serve --project /srv/my-links \
|
|
20
|
+
--host 127.0.0.1 --port 3000 --origin https://links.example.com
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`--origin` defines the public URL seen by functions; proxy Host/X-Forwarded-*
|
|
24
|
+
headers are intentionally not trusted. Use a process supervisor that restarts on
|
|
25
|
+
failure and sends SIGTERM for shutdown. Shutdown stops accepting requests, gives
|
|
26
|
+
HTTP connections up to 10 seconds, and drains bounded in-flight functions.
|
|
27
|
+
|
|
28
|
+
Serve a read-only application tree where practical. The operator-owned runtime
|
|
29
|
+
account must be able to read application files/dependencies. Authoring happens
|
|
30
|
+
in development/CI, not by modifying a running replica's filesystem.
|
|
31
|
+
|
|
32
|
+
## Container deployment
|
|
33
|
+
|
|
34
|
+
The supplied image packages the runtime; it does not copy your application or
|
|
35
|
+
local secret files. Build from the reviewed runtime checkout:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
docker build -t urlcode:0.2.0 .
|
|
39
|
+
docker run --rm --name my-links \
|
|
40
|
+
--read-only --cap-drop ALL --security-opt no-new-privileges \
|
|
41
|
+
--memory 512m --cpus 1 --pids-limit 128 \
|
|
42
|
+
-p 127.0.0.1:3000:3000 \
|
|
43
|
+
-v "$PWD/starters/default:/project:ro" \
|
|
44
|
+
urlcode:0.2.0
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Replace the example mount with your app. The image uses the unprivileged `node`
|
|
48
|
+
user; ensure mounted config/functions are readable by it. Only operator-owned components such as the optional link store can use writable
|
|
49
|
+
mounts. Sandboxed application functions cannot access mounted files or installed
|
|
50
|
+
Node packages. The resource values above illustrate
|
|
51
|
+
container limits, not a sizing recommendation; large configuration compilation
|
|
52
|
+
can need more memory. Measure your workload. Tag/redeploy immutable image digests
|
|
53
|
+
in real operation rather than treating a mutable tag as a rollback identity.
|
|
54
|
+
|
|
55
|
+
## Domains, HTTPS and exposure
|
|
56
|
+
|
|
57
|
+
Point your domain's DNS at the reverse proxy/load balancer you operate, terminate
|
|
58
|
+
HTTPS there, and forward to the loopback/private URLCode port. Use a tested proxy
|
|
59
|
+
such as your existing Caddy/nginx/load-balancer setup for certificates, connection
|
|
60
|
+
limits and rate limiting. No certificate automation is supplied by URLCode yet.
|
|
61
|
+
Keep direct backend access private. Restrict `/_urlcode/*` endpoints to operators
|
|
62
|
+
at the proxy; they are unauthenticated and reveal route count/config digest.
|
|
63
|
+
|
|
64
|
+
If functions perform sensitive actions, implement authentication and authorization
|
|
65
|
+
in the application. A short URL is not automatically an access-control mechanism.
|
|
66
|
+
Functions are untrusted and isolated in WASM by default. Keep separate deployment
|
|
67
|
+
processes/containers and narrowly scoped credentials as additional boundaries.
|
|
68
|
+
Do not expose a public code-upload/multi-tenant service on the basis of the self-hosted release alone
|
|
69
|
+
without separate security review and stronger service-level containment.
|
|
70
|
+
|
|
71
|
+
## Secrets and rotation
|
|
72
|
+
|
|
73
|
+
`dev`, `test`, `routes`, `audit`, `benchmark` and `validate --local` read
|
|
74
|
+
`.env.local`. Authoring and
|
|
75
|
+
permissions inspection do not read credentials or execute functions. `serve` and
|
|
76
|
+
ordinary `validate` use process environment only. Resolve logical names from
|
|
77
|
+
your own secret store/supervisor and inject them at startup; direct provider
|
|
78
|
+
secret-store integrations remain future work. Bindings also require an external,
|
|
79
|
+
revision-pinned operator policy; see [setup](FUNCTION-SECURITY.md). Do not place secret values in
|
|
80
|
+
command-line arguments, route YAML, image layers or Git.
|
|
81
|
+
|
|
82
|
+
Check tracked files as well as ignore rules. Docker builds use an explicit
|
|
83
|
+
allowlist; npm artifacts include runtime/schema/starter/docs files only. Do not
|
|
84
|
+
build an application image by blindly copying its entire development directory.
|
|
85
|
+
Rotate a credential by replacing its injected value and restarting/redeploying;
|
|
86
|
+
production does not watch or refresh secret values automatically.
|
|
87
|
+
|
|
88
|
+
## Health, logs and limits
|
|
89
|
+
|
|
90
|
+
- `GET /_urlcode/health`: process liveness.
|
|
91
|
+
- `GET /_urlcode/ready`: 200 when the active snapshot and all function workers
|
|
92
|
+
are available and configured link-store readers are healthy; 503 while a worker/store
|
|
93
|
+
is unavailable. Busy workers alone do not
|
|
94
|
+
mark readiness down. A failed worker or store connection is replaced with
|
|
95
|
+
exponential backoff (250 ms doubling to a 30-second ceiling) and readiness
|
|
96
|
+
reports 503 until every slot is serving again. Replacement does not stop, so a
|
|
97
|
+
request-triggered deadline cannot disable functions until an operator restarts;
|
|
98
|
+
a cause that keeps recurring keeps the instance shedding load and needs an
|
|
99
|
+
operator. Alert on sustained `function_worker`/`link_store_worker` restart events.
|
|
100
|
+
- Probes are answered from their own admission budget (16 by default,
|
|
101
|
+
`--max-in-flight-health`), so they stay available while the application is
|
|
102
|
+
saturated without being an unmetered endpoint. They are unauthenticated and
|
|
103
|
+
report the configuration digest and route count: keep them on an internal
|
|
104
|
+
interface or restrict them at the ingress.
|
|
105
|
+
- Request logs: JSON request ID, status and duration. No URLs, query strings,
|
|
106
|
+
headers, bodies, bindings or user exception text. `--request-log detailed` adds
|
|
107
|
+
the request method and the matched route pattern (`/u/{id}`, or `null` when
|
|
108
|
+
nothing matched). Both come from the reviewed configuration, never from
|
|
109
|
+
request-supplied path, parameter or query text, which is what makes per-route
|
|
110
|
+
error rates and latency available without logging user data.
|
|
111
|
+
- Request IDs are generated per request and returned in `x-request-id`. An
|
|
112
|
+
inbound `x-request-id` is ignored unless `--trust-request-id` is set, which is
|
|
113
|
+
only correct when a trusted proxy sets the header and strips client-supplied
|
|
114
|
+
copies; untrusted values are still rejected unless they are a single header of
|
|
115
|
+
at most 128 characters from `[A-Za-z0-9_.:-]`. Forward stdout to your log
|
|
116
|
+
system and alert on sustained 5xx and latency. The default logger drops records
|
|
117
|
+
when stdout buffering reaches 1 MiB and reports the dropped count when output
|
|
118
|
+
recovers; alert on `logs_dropped`. Function console output is
|
|
119
|
+
suppressed; app-specific diagnostics are not yet a first-class feature.
|
|
120
|
+
Synchronous and asynchronous sink failures are contained; a failed sink drops
|
|
121
|
+
subsequent output and needs operator recovery. Collectors own rotation/retention.
|
|
122
|
+
- HTTP: 8,192-character target, 16 KiB headers, 1 MiB buffered body, 15-second request
|
|
123
|
+
receipt timeout, 10-second header timeout, 5-second keep-alive, 1,000 requests
|
|
124
|
+
per socket and 1,024 active connections. At most 64 application requests are
|
|
125
|
+
admitted through response completion; excess requests receive 503. Health probes
|
|
126
|
+
remain available under admission saturation. A 15-second socket inactivity
|
|
127
|
+
timeout closes stalled readers/writers. Proxy timeouts/rate limits still matter.
|
|
128
|
+
- Functions: 2 concurrent workers (`--workers`), no queue, 5-second deadline
|
|
129
|
+
(`--function-timeout-ms`), 1 MiB buffered response (`--max-response-bytes`) and
|
|
130
|
+
16 KiB response headers. Saturation 503; timeout 504; error 502.
|
|
131
|
+
QuickJS guests have a 32 MiB heap and 512 KiB stack budget and no network or
|
|
132
|
+
host capabilities. Outer workers have additional V8 limits. Total process/WASM
|
|
133
|
+
memory still needs deployment-level limits; do not equate guest budget with RSS.
|
|
134
|
+
|
|
135
|
+
`urlcode serve`/`dev` and the JavaScript server API both configure workers,
|
|
136
|
+
deadlines and byte limits: `--workers`, `--function-timeout-ms`,
|
|
137
|
+
`--max-response-bytes`, `--max-body-bytes`, `--max-in-flight` and
|
|
138
|
+
`--max-in-flight-health`. Set them on the container command line; these are
|
|
139
|
+
deployment controls, not portable route behavior. Horizontal replicas
|
|
140
|
+
must use identical application/config versions and secret bindings. In-memory
|
|
141
|
+
function state is reset after every invocation, not durable/shared application state.
|
|
142
|
+
General application storage needs a future explicit capability broker; no
|
|
143
|
+
storage/network access is exposed to the guest. The optional native
|
|
144
|
+
[link store](DYNAMIC-LINKS.md) supports live short-link records on one host.
|
|
145
|
+
|
|
146
|
+
The health version combines route-definition and asset-representation digests;
|
|
147
|
+
it does not identify the complete function/runtime release. Record runtime commit,
|
|
148
|
+
application commit, dependency locks and image digest in your deployment system.
|
|
149
|
+
|
|
150
|
+
## Optional dynamic-link deployment
|
|
151
|
+
|
|
152
|
+
Keep SQLite and management tokens outside the application, in a private durable
|
|
153
|
+
local directory. Initialize through `links init/create`, bind public serving with
|
|
154
|
+
`--link-store`, and expose management on a separate private listener. Restrict
|
|
155
|
+
its token to your trusted backend; apply ingress limits and backups. Public
|
|
156
|
+
serving opens read-only pools; management has a separate writer and read pool.
|
|
157
|
+
Budget connections across processes and monitor writer health separately. See
|
|
158
|
+
[dynamic-link operations](DYNAMIC-LINKS.md). Multiple host replicas must not share
|
|
159
|
+
this file over a network filesystem; no distributed adapter is included yet.
|
|
160
|
+
|
|
161
|
+
## Deployment and rollback procedure
|
|
162
|
+
|
|
163
|
+
1. Build a candidate from pinned runtime/application revisions and lockfiles.
|
|
164
|
+
2. Validate its config/bindings and run local HTTP tests without external redirects.
|
|
165
|
+
3. Start it on an alternate private port/container. Check readiness and representative
|
|
166
|
+
redirect/function behavior through the intended proxy configuration.
|
|
167
|
+
4. Switch proxy traffic after checks pass. Drain the previous instance before stopping.
|
|
168
|
+
5. If checks or observed behavior fail, route traffic back to the retained previous
|
|
169
|
+
instance/image and its compatible secret bindings.
|
|
170
|
+
|
|
171
|
+
This is an operator procedure, not an implemented deployment control plane.
|
|
172
|
+
Rollback cannot undo a function's external side effects or migrate an app's
|
|
173
|
+
state automatically. Plan those independently. Keep Git definitions backed up;
|
|
174
|
+
back up any app-owned persistent state separately. YAML routes require no database; dynamic link records require separate backups.
|
|
175
|
+
|
|
176
|
+
## Capacity and incident planning
|
|
177
|
+
|
|
178
|
+
See [capacity and concurrency](CAPACITY.md) for hard limits, worker occupancy,
|
|
179
|
+
no-queue rejection, memory/reload budgets and theoretical sizing. See
|
|
180
|
+
[DDoS and recovery](RESILIENCE.md) for ingress responsibilities, incident response,
|
|
181
|
+
rollback/restore procedures, recovery objectives and drills.
|
|
182
|
+
|
|
183
|
+
See the [release-readiness register](RELEASE-READINESS.md) for evidence and open gates.
|
|
184
|
+
|
|
185
|
+
## Remaining production validation
|
|
186
|
+
|
|
187
|
+
Before approving a production deployment: run sustained soak/load tests on its
|
|
188
|
+
hardware, obtain independent security review, exercise failure/restart and
|
|
189
|
+
upgrade/rollback, establish a clear support/reporting policy, and add the needed
|
|
190
|
+
operational metrics. Provider adapters remain separate roadmap work. See
|
|
191
|
+
[roadmap](../ROADMAP.md). No claim of high
|
|
192
|
+
availability, zero downtime or provider portability beyond the Node process
|
|
193
|
+
adapter is made by the current release.
|
|
194
|
+
|
|
195
|
+
## Security review
|
|
196
|
+
|
|
197
|
+
The [2026-09-16 internal audit](SECURITY-AUDIT.md) records fixes, regression evidence
|
|
198
|
+
and remaining security/operational gates. This is not an independent assessment.
|
|
199
|
+
|
|
200
|
+
## Management hardening baseline
|
|
201
|
+
|
|
202
|
+
Management is now restricted to literal loopback addresses. Prefer `--auth-file`
|
|
203
|
+
for individual expiring, revocable credentials with collection/action scopes.
|
|
204
|
+
Every successful built-in store mutation has an atomic, durable SQLite audit row;
|
|
205
|
+
HTTP request logs remain best effort. See [management security](MANAGEMENT-SECURITY.md)
|
|
206
|
+
for policy examples, compatibility, archival and rollback requirements, and
|
|
207
|
+
[operational proof](OPERATIONAL-PROOF.md) for executable recovery drills.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Organize routes your way
|
|
2
|
+
|
|
3
|
+
`urlcode.yaml` is the project entry point. The rest of the folder structure is
|
|
4
|
+
yours. Use one file for a few links or split routes by feature, team, campaign,
|
|
5
|
+
customer or another useful grouping. Folder names do not create URL prefixes.
|
|
6
|
+
Explicit file composition is already implemented; no new runtime mode is needed.
|
|
7
|
+
|
|
8
|
+
## One file
|
|
9
|
+
|
|
10
|
+
```yaml
|
|
11
|
+
# urlcode.yaml
|
|
12
|
+
version: "1"
|
|
13
|
+
routes:
|
|
14
|
+
/go:
|
|
15
|
+
redirect:
|
|
16
|
+
url: https://example.com
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Multiple files and nested folders
|
|
20
|
+
|
|
21
|
+
The public [urlcode-template](https://github.com/jimhoyd-com/urlcode-template)
|
|
22
|
+
contains a function route and a redirect, organized like this:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
urlcode.yaml
|
|
26
|
+
routes/
|
|
27
|
+
functions.yaml
|
|
28
|
+
marketing/
|
|
29
|
+
links.yaml
|
|
30
|
+
functions/
|
|
31
|
+
hello.mjs
|
|
32
|
+
tests/
|
|
33
|
+
requests.json
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Entry point:
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
# urlcode.yaml
|
|
40
|
+
version: "1"
|
|
41
|
+
includes:
|
|
42
|
+
- routes/functions.yaml
|
|
43
|
+
- routes/marketing/links.yaml
|
|
44
|
+
routes: {}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Function route:
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
# routes/functions.yaml
|
|
51
|
+
version: "1"
|
|
52
|
+
routes:
|
|
53
|
+
/hello:
|
|
54
|
+
function:
|
|
55
|
+
source: functions/hello.mjs
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Function source for this short example:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
// functions/hello.mjs
|
|
62
|
+
export default function hello() {
|
|
63
|
+
return Response.json({ message: "Hello!" });
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Redirect route:
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
# routes/marketing/links.yaml
|
|
71
|
+
version: "1"
|
|
72
|
+
routes:
|
|
73
|
+
/go:
|
|
74
|
+
redirect:
|
|
75
|
+
url: https://example.com
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
These produce `/hello` and `/go`, not `/routes/hello` or `/marketing/go`. The
|
|
79
|
+
actual template adds a validated `{name}` input to the function example. You can
|
|
80
|
+
also colocate code with a feature, for example `features/support/routes.yaml`
|
|
81
|
+
and `features/support/hello.mjs`; use `source: features/support/hello.mjs`.
|
|
82
|
+
|
|
83
|
+
## Mix inline and included routes
|
|
84
|
+
|
|
85
|
+
Keep a few common routes in the entry point while splitting larger groups:
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
version: "1"
|
|
89
|
+
includes:
|
|
90
|
+
- routes/functions.yaml
|
|
91
|
+
- routes/marketing/links.yaml
|
|
92
|
+
routes:
|
|
93
|
+
/status:
|
|
94
|
+
respond:
|
|
95
|
+
json: {ok: true}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
With the example files above, the combined project has three routes. Update your
|
|
99
|
+
reviewed `--expect-routes` count when adding/removing a route. Simply moving a
|
|
100
|
+
route between files does not change the count or its URL.
|
|
101
|
+
|
|
102
|
+
## Composition rules
|
|
103
|
+
|
|
104
|
+
- All file references are relative to the project root containing `urlcode.yaml`,
|
|
105
|
+
including `includes`, function `source`, and asset `file`/`directory` references.
|
|
106
|
+
They are never relative to the included YAML file.
|
|
107
|
+
- Each file declares `version: "1"` and `routes`. The entry point uses `routes: {}`
|
|
108
|
+
when all routes live in includes. `.yaml` and `.yml` work.
|
|
109
|
+
- Includes list explicit files, including paths through nested folders. There is
|
|
110
|
+
no directory auto-discovery, glob expansion or remote configuration download.
|
|
111
|
+
- Put all includes in `urlcode.yaml`; included files cannot include other files.
|
|
112
|
+
- Duplicate files or route paths fail validation. Include order does not provide
|
|
113
|
+
overrides or change route-matching precedence.
|
|
114
|
+
- References must stay inside the project. Missing files and escaping symlinks
|
|
115
|
+
fail validation. Do not use secret files as configuration.
|
|
116
|
+
- Current limits: 256 included files, 32 MiB per YAML file, 64 MiB aggregate YAML and 100,000 total routes. Parser-worker and compilation limits also apply; see [capacity](CAPACITY.md).
|
|
117
|
+
These limits apply to the combined project, not separately per folder.
|
|
118
|
+
|
|
119
|
+
`dev` reloads changes to ordinary project YAML files and keeps the last valid
|
|
120
|
+
snapshot if a change is invalid. Configuration in hidden/build/dependency
|
|
121
|
+
folders or behind symlink targets is outside the normal watcher: use ordinary
|
|
122
|
+
source folders or restart after such changes. File grouping does not weaken
|
|
123
|
+
function isolation or operator binding requirements.
|
|
124
|
+
|
|
125
|
+
`validate`, `routes`, `test`, `audit` and `benchmark` all use the same merged
|
|
126
|
+
project. To move files safely: edit the include list and any changed project-root
|
|
127
|
+
references, validate, then run the audit with the same expected count. Paths
|
|
128
|
+
inside a moved route file need no change when their target files remain in place.
|
|
129
|
+
See the [implemented contract](SPECIFICATION.md) and [readiness guide](READINESS.md).
|
|
130
|
+
|
|
131
|
+
See [route matching and new links](ROUTING.md) for parameter/wildcard semantics,
|
|
132
|
+
priority rules and activation of newly added definitions.
|
|
133
|
+
|
|
134
|
+
For layout choices, naming, readable YAML/functions, middleware responsibilities
|
|
135
|
+
and safe refactoring, see [organization and readability practices](BEST-PRACTICES.md).
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Performance checks
|
|
2
|
+
|
|
3
|
+
Run from the runtime checkout:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm run benchmark -- 1000
|
|
7
|
+
npm run benchmark -- 10000
|
|
8
|
+
npm run benchmark -- 100000
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Each invocation generates a temporary literal-redirect collection, measures
|
|
12
|
+
YAML load/compile/server startup, warms up 100 requests, then runs 5,000 real
|
|
13
|
+
loopback HTTP requests with 16 keep-alive connections. It verifies every status
|
|
14
|
+
and destination, never follows redirects, cleans up its process/files and
|
|
15
|
+
prints JSON with hardware/runtime, startup, memory, throughput and latency.
|
|
16
|
+
|
|
17
|
+
Initial local measurements on 2026-09-16: Apple M4 Pro, 48 GiB RAM, macOS arm64,
|
|
18
|
+
Node 26.8.2. These are short development runs on a shared machine, not an SLA,
|
|
19
|
+
sizing recommendation, soak result or comparison against other products.
|
|
20
|
+
|
|
21
|
+
| Routes | Startup | RSS after startup | Redirect requests/s | p95 latency |
|
|
22
|
+
|---|---:|---:|---:|---:|
|
|
23
|
+
| 1,000 | 29 ms | 86 MiB | 29,398 | 0.87 ms |
|
|
24
|
+
| 10,000 | 168 ms | 186 MiB | 28,458 | 0.85 ms |
|
|
25
|
+
| 100,000 (after parser fix) | 1,431 ms | 621 MiB | 28,563 | 0.85 ms |
|
|
26
|
+
|
|
27
|
+
These runs use the improved parser; the original 100,000-route startup took
|
|
28
|
+
42,493 ms. A string-key-only duplicate check reduced that to
|
|
29
|
+
1,431 ms while retaining duplicate rejection tests. Parser transient allocation
|
|
30
|
+
still contributes significantly to RSS; 100k startup exceeds the illustrative
|
|
31
|
+
512 MiB container limit. Do not size from routing data alone.
|
|
32
|
+
|
|
33
|
+
Before a stable production release, repeat on supported Node 22/24/26 deployment
|
|
34
|
+
hardware, measure long-running memory/reloads and tail latency under sustained
|
|
35
|
+
traffic, include mixed parameter/function workloads and overload/recovery, and
|
|
36
|
+
set regression budgets from repeatable evidence. Worker concurrency and slow
|
|
37
|
+
upstream services need separate tests; literal redirect throughput does not
|
|
38
|
+
predict function throughput. Runtime logs were disabled for these measurements.
|
|
39
|
+
|
|
40
|
+
The 5,000 measured requests do not exercise every route in the larger datasets.
|
|
41
|
+
Client and server share a process; these runs exclude TLS/proxy overhead and
|
|
42
|
+
production logging. No NGINX comparison has been measured. Use the
|
|
43
|
+
[capacity model](CAPACITY.md) and [recovery drills](RESILIENCE.md) when designing
|
|
44
|
+
a deployment benchmark; do not extrapolate native redirect RPS to middleware.
|
|
45
|
+
|
|
46
|
+
## TypeScript conversion, 2026-09-17
|
|
47
|
+
|
|
48
|
+
The runtime source became TypeScript, shipped as `dist/`: Node's own type
|
|
49
|
+
stripping with the specifier extension rewritten, so `dist/x.js` is
|
|
50
|
+
byte-equivalent to `src/x.ts` with the types removed (same lines, same
|
|
51
|
+
columns; no bundling, minification or syntax transform). The published
|
|
52
|
+
package therefore runs the same JavaScript it ran before. Measured on the same
|
|
53
|
+
machine (Intel Xeon 2.80 GHz, 16 GiB, Linux, Node 22.22.2), a plain-JavaScript
|
|
54
|
+
worktree of the pre-conversion commit against `dist/cli.js`, three alternating
|
|
55
|
+
runs each, medians of medians:
|
|
56
|
+
|
|
57
|
+
| Check | Before (JavaScript source) | After (`dist/`) |
|
|
58
|
+
|---|---:|---:|
|
|
59
|
+
| CLI cold start, `validate --project starters/default` (20 spawns) | 798.6 ms | 797.8 ms |
|
|
60
|
+
| CLI cold start, `routes --project starters/default` | 795.1 ms | 770.9 ms |
|
|
61
|
+
| CLI cold start, `--help` | 331.0 ms | 328.4 ms |
|
|
62
|
+
| `npm run benchmark -- 1000` requests/s (6 runs) | 4,640–4,927 | 4,657–4,914 |
|
|
63
|
+
| `npm run benchmark -- 1000` p95 | 5.77–6.00 ms | 5.65–5.96 ms |
|
|
64
|
+
| `npm run benchmark -- 1000` RSS after startup | 95–100 MiB | 95–100 MiB |
|
|
65
|
+
| `cli benchmark --project starters/default` (function route) | 1,188 req/s, p95 3.50 ms | 1,265 req/s, p95 3.28 ms |
|
|
66
|
+
|
|
67
|
+
Every difference is inside run-to-run spread; the gates (cold start within 2 %,
|
|
68
|
+
throughput within noise, RSS within 5 MiB) were met. Running the `.ts` source
|
|
69
|
+
directly (`node src/cli.ts`, the developer loop) costs about +190 ms of cold
|
|
70
|
+
start and +35 MiB RSS for the stripping itself; that mode never ships.
|
|
71
|
+
Container time-to-ready was not measured here (no Docker in that environment);
|
|
72
|
+
CI's readiness loop covers it.
|
package/docs/PLUGINS.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Host plugins
|
|
2
|
+
|
|
3
|
+
A plugin is host code an operator passes to the runtime in JavaScript or
|
|
4
|
+
TypeScript. It sees
|
|
5
|
+
every request after the route is matched and before the handler runs, may
|
|
6
|
+
answer it outright, and sees every response before it is written. The
|
|
7
|
+
declarative [policies](POLICIES.md) are implemented on the same hook names,
|
|
8
|
+
so first-party and operator behavior share one code path and one test seam.
|
|
9
|
+
|
|
10
|
+
Plugins are not part of the project format. Nothing in YAML names a plugin or
|
|
11
|
+
an npm package; the operator application passes them to `startServer`,
|
|
12
|
+
`createRuntime`, `createVercelHandler` or `createLambdaHandler`. A project stays
|
|
13
|
+
portable while an operator can still add a shared-store limiter, a verified-bot
|
|
14
|
+
check or a purge endpoint. Plugins run in the host process with the host's
|
|
15
|
+
privileges: they are the operator's trust boundary, not the project's, and the
|
|
16
|
+
runtime does not sandbox, review or limit what a plugin does with a request.
|
|
17
|
+
Treat a plugin like any other dependency of the operator application.
|
|
18
|
+
|
|
19
|
+
## Passing plugins
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
import { startServer } from '@jimhoyd/urlcode';
|
|
23
|
+
|
|
24
|
+
await startServer({
|
|
25
|
+
project: './site',
|
|
26
|
+
trustedProxies: ['10.0.0.0/8'],
|
|
27
|
+
plugins: [auditPlugin],
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`createRuntime(project, { plugins })` takes the same array for embedding
|
|
32
|
+
without the HTTP server. `createVercelHandler({ plugins })` and
|
|
33
|
+
`createLambdaHandler({ plugins })` pass it through to the adapter's runtime
|
|
34
|
+
with the target set to `vercel` or `aws`. The Cloudflare build takes no
|
|
35
|
+
plugins: the Worker carries only compiled policies. `runtime.plugins` lists
|
|
36
|
+
the activated `{ name, version }` pairs.
|
|
37
|
+
|
|
38
|
+
## Plugin shape
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
const auditPlugin = {
|
|
42
|
+
name: 'audit', // ^[a-z][a-z0-9-]{0,63}$, unique per runtime
|
|
43
|
+
version: '1.0.0', // any string up to 64 characters
|
|
44
|
+
targets: ['node', 'vercel'], // subset of node, vercel, aws, cloudflare
|
|
45
|
+
async onActivate(runtime) {}, // may throw to refuse activation
|
|
46
|
+
async onRequest(request) {}, // return a result to short-circuit
|
|
47
|
+
async onResponse(request, result) { return result; }, // return the result to send
|
|
48
|
+
onError(request, error) {}, // observe a thrown error
|
|
49
|
+
async onClose() {}, // release resources
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The package ships declarations for this contract: `Plugin` and
|
|
54
|
+
`PluginRuntime` (what `onActivate` receives) from `@jimhoyd/urlcode/plugins`, with
|
|
55
|
+
`PolicyRequest`, `HandlerResult`, `HeaderPair`, `TargetName` and `TestPlan`
|
|
56
|
+
re-exported beside them, and `HostPlugin` (the same type) from `urlcode`. The
|
|
57
|
+
same plugin in TypeScript:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import type { Plugin, PolicyRequest, HandlerResult } from '@jimhoyd/urlcode/plugins';
|
|
61
|
+
|
|
62
|
+
const auditPlugin: Plugin = {
|
|
63
|
+
name: 'audit',
|
|
64
|
+
version: '1.0.0',
|
|
65
|
+
targets: ['node', 'vercel'],
|
|
66
|
+
onRequest(request: PolicyRequest): HandlerResult | undefined {
|
|
67
|
+
if (request.path === '/deny') return { status: 451, headers: [], body: new Uint8Array(0) };
|
|
68
|
+
return undefined;
|
|
69
|
+
},
|
|
70
|
+
onResponse(request, result) { return { ...result, headers: [...result.headers, ['x-plugin', 'seen']] }; },
|
|
71
|
+
};
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Validation happens at activation, before any request: at most 32 plugins,
|
|
75
|
+
each an object with a kebab-case `name` no other plugin uses, a `version`
|
|
76
|
+
string and a `targets` array of known target names; every declared hook must
|
|
77
|
+
be a function and at least one must be present. A plugin whose `targets` does
|
|
78
|
+
not include the runtime's target is refused with
|
|
79
|
+
`Plugin "name" does not support the vercel target`, mirroring how adapters
|
|
80
|
+
refuse routes, so the same operator application fails fast where a plugin's
|
|
81
|
+
assumptions do not hold.
|
|
82
|
+
|
|
83
|
+
`onActivate(runtime)` receives `{ testPlan(), version, root, target }`.
|
|
84
|
+
`testPlan()` returns the route inventory (`inventory[].policies` names the
|
|
85
|
+
effective policies per route) and `policies`, the per-route policy summaries
|
|
86
|
+
with their `target` value, so a plugin can inspect what it is joining and
|
|
87
|
+
throw to refuse. Hooks may be async; the runtime awaits them.
|
|
88
|
+
|
|
89
|
+
## The request object
|
|
90
|
+
|
|
91
|
+
Every request hook receives the same object the policies see, built once per
|
|
92
|
+
request by `policyRequest` in `src/policies.ts`:
|
|
93
|
+
|
|
94
|
+
| Field | Value |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `method` | Request method, upper case |
|
|
97
|
+
| `target` | Raw request target (path plus query string) as received |
|
|
98
|
+
| `path` | Parsed path |
|
|
99
|
+
| `params` | Route parameter values by name |
|
|
100
|
+
| `query` | Parsed query as `URLSearchParams` |
|
|
101
|
+
| `headers` | Request `Headers` |
|
|
102
|
+
| `headerCounts` | Occurrences per header name, lower case, so a duplicated scalar header can be detected (empty on Cloudflare, where the platform joins repeats) |
|
|
103
|
+
| `client` | Client address as the host resolved it through `--trusted-proxies` or the platform's connection address, or `null` when none could be resolved. Never a raw forwarded header |
|
|
104
|
+
| `origin` | Public origin the runtime was told (`--origin`, `URLCODE_ORIGIN` or the platform's), used for HSTS and absolute URLs |
|
|
105
|
+
| `route` | The matched route's configured pattern, never request text |
|
|
106
|
+
| `secrets` | `true` when the route declares secret bindings |
|
|
107
|
+
|
|
108
|
+
The object identity is stable for the life of the request, so a plugin may key
|
|
109
|
+
a `WeakMap` on it to carry state from `onRequest` to `onResponse`. Nothing on
|
|
110
|
+
it reaches the guest: there is no sandbox handle, no deadline, no `env` or
|
|
111
|
+
`secrets` values and no binding. A plugin cannot extend or shorten a function's
|
|
112
|
+
deadline, read or write guest state, or obtain a binding the operator policy
|
|
113
|
+
did not grant to the route.
|
|
114
|
+
|
|
115
|
+
## Results
|
|
116
|
+
|
|
117
|
+
A result is what the runtime writes:
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
{ status: 429, headers: [['content-type', 'text/plain; charset=utf-8'], ['retry-after', '30']], body: Buffer.from('Too many requests\n') }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`headers` is an array of `[name, value]` pairs; `body` is a `Buffer`
|
|
124
|
+
(a `Uint8Array` on Cloudflare). `onRequest` returns such a result to answer the
|
|
125
|
+
request without running the handler, or `undefined` to continue. `onResponse`
|
|
126
|
+
returns the result to send; returning `undefined` keeps the one it was given.
|
|
127
|
+
Replace rather than mutate. Every result still passes through the shared
|
|
128
|
+
response writer on the way out: the status must be an integer from 200 to 599,
|
|
129
|
+
header names and values are validated (an invalid one fails the request as a
|
|
130
|
+
function's would), hop-by-hop headers and `Content-Length` are dropped and
|
|
131
|
+
recomputed, `X-Request-Id` and `X-Content-Type-Options: nosniff` are added, and
|
|
132
|
+
`Cache-Control: no-store` is added when the result carries no `Cache-Control`.
|
|
133
|
+
HEAD and 204/205/304 results are sent without a body.
|
|
134
|
+
|
|
135
|
+
## Ordering
|
|
136
|
+
|
|
137
|
+
Plugins wrap everything else. On the request side the first plugin in the
|
|
138
|
+
array runs first, then the second, then the policies (`agents`, `throttle`,
|
|
139
|
+
`cache`), then the route contract and the handler. On the response side the
|
|
140
|
+
policies run, then plugins in reverse: the first plugin sees the request first
|
|
141
|
+
and the response last, the standard onion. A short-circuit from any
|
|
142
|
+
`onRequest` skips the remaining plugins, every policy request hook and the
|
|
143
|
+
handler, and goes straight to the response side; the response hooks of
|
|
144
|
+
policies with a request phase are skipped for it (nothing is cached, no
|
|
145
|
+
rate-limit headers), security headers and compression still apply, and every
|
|
146
|
+
plugin's `onResponse` still runs.
|
|
147
|
+
|
|
148
|
+
`onError` hooks run in reverse order for an error the runtime throws after
|
|
149
|
+
the request object exists (404 for a disabled route is thrown before it; a
|
|
150
|
+
405 is an ordinary result and reaches `onResponse`). They observe only: a
|
|
151
|
+
throw inside `onError` is swallowed and the outcome stands. A policy's error
|
|
152
|
+
hook may answer with a fallback instead; when one does, plugin `onError`
|
|
153
|
+
hooks do not run and plugin `onResponse` hooks see the fallback (no
|
|
154
|
+
first-party policy returns one today). `onClose` runs in reverse order when
|
|
155
|
+
the runtime closes, after the policies have released their state; a throw
|
|
156
|
+
there is ignored.
|
|
157
|
+
|
|
158
|
+
On a reload the same plugin objects are activated again: `onActivate` runs
|
|
159
|
+
for the new runtime before `onClose` runs for the retired one, so plugin
|
|
160
|
+
state persists unless `onClose` discards it. An `onActivate` that throws
|
|
161
|
+
rejects the reload and the old runtime keeps serving.
|
|
162
|
+
|
|
163
|
+
## What a plugin cannot do
|
|
164
|
+
|
|
165
|
+
- Reach inside the guest, extend a deadline or see bindings: the request
|
|
166
|
+
object carries none of those, and there is no other handle.
|
|
167
|
+
- Change a project's routes or policies after activation; `onActivate` can
|
|
168
|
+
refuse, not rewrite.
|
|
169
|
+
- Be selected from YAML. A project cannot require a plugin, and a plugin's
|
|
170
|
+
presence does not change how the project validates.
|
|
171
|
+
- Change a thrown error's status or body from `onError`.
|
|
172
|
+
- Run on Cloudflare: the Worker carries compiled policies only.
|
|
173
|
+
|
|
174
|
+
## Sketches
|
|
175
|
+
|
|
176
|
+
Both examples are sketches, not shipped code: they omit error handling,
|
|
177
|
+
configuration and the store or verifier they depend on.
|
|
178
|
+
|
|
179
|
+
### Shared-store throttle
|
|
180
|
+
|
|
181
|
+
The built-in `throttle` keeps counters per instance. Across replicas an
|
|
182
|
+
operator needs a shared store; a plugin keys it on the resolved client and the
|
|
183
|
+
route pattern, both of which the request object already carries.
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
// Sketch. `store.increment(key, windowSeconds)` is a fixed-window counter
|
|
187
|
+
// in a shared store and returns the count after increment.
|
|
188
|
+
export function sharedThrottle({ store, quota, window }) {
|
|
189
|
+
return {
|
|
190
|
+
name: 'shared-throttle', version: '0.1.0', targets: ['node', 'vercel', 'aws'],
|
|
191
|
+
async onRequest(req) {
|
|
192
|
+
const key = `${req.route}|${req.client ?? 'shared'}`;
|
|
193
|
+
const used = await store.increment(key, window);
|
|
194
|
+
if (used <= quota) return undefined;
|
|
195
|
+
return {
|
|
196
|
+
status: 429,
|
|
197
|
+
headers: [['content-type', 'text/plain; charset=utf-8'], ['cache-control', 'no-store'], ['retry-after', String(window)]],
|
|
198
|
+
body: Buffer.from('Too many requests\n'),
|
|
199
|
+
};
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Combine it with `policies.throttle: false` on the routes it covers, or keep
|
|
206
|
+
the built-in policy as a per-instance ceiling underneath it.
|
|
207
|
+
|
|
208
|
+
### Verified-bot allow
|
|
209
|
+
|
|
210
|
+
The `agents` policy matches strings only. Verifying that a request claiming to
|
|
211
|
+
be a search crawler really comes from one (reverse DNS as the major engines
|
|
212
|
+
document, or an HTTP Message Signature per the web-bot-auth drafts) needs a
|
|
213
|
+
network lookup and a cache, so it is a plugin. The sketch answers 403 to a
|
|
214
|
+
claimed crawler whose address does not verify and lets everything else
|
|
215
|
+
continue to the `agents` policy.
|
|
216
|
+
|
|
217
|
+
```js
|
|
218
|
+
// Sketch. `verify(address)` resolves the address back to the crawler's
|
|
219
|
+
// documented domains and caches the answer; it is not part of the runtime.
|
|
220
|
+
export function verifiedBots({ verify, claims = /Googlebot|bingbot/i }) {
|
|
221
|
+
return {
|
|
222
|
+
name: 'verified-bots', version: '0.1.0', targets: ['node'],
|
|
223
|
+
async onRequest(req) {
|
|
224
|
+
const agent = req.headers.get('user-agent') || '';
|
|
225
|
+
if (!claims.test(agent) || !req.client) return undefined;
|
|
226
|
+
if (await verify(req.client)) return undefined;
|
|
227
|
+
return { status: 403, headers: [['content-type', 'text/plain; charset=utf-8'], ['cache-control', 'no-store']], body: Buffer.from('Forbidden\n') };
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
A plugin that logs should follow the policies' rule: record the route pattern
|
|
234
|
+
and the outcome, not the client address or the User-Agent string.
|