@jimhoyd/urlcode 0.4.2 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/urlcode-authoring/SKILL.md +57 -14
- package/.claude/skills/urlcode-operations/SKILL.md +4 -0
- package/README.md +19 -15
- 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 +43 -5
- package/package.json +15 -22
- 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
package/docs/ORGANIZATION.md
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
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).
|
package/docs/PERFORMANCE.md
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,271 +0,0 @@
|
|
|
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
|
-
credentialHeaders: ['Cookie', 'Authorization'], // optional; withheld from guests
|
|
46
|
-
async onActivate(runtime) {}, // may throw to refuse activation
|
|
47
|
-
async onRequest(request) {}, // return a result to short-circuit
|
|
48
|
-
async onResponse(request, result) { return result; }, // return the result to send
|
|
49
|
-
onError(request, error) {}, // observe a thrown error
|
|
50
|
-
async onClose() {}, // release resources
|
|
51
|
-
};
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
The package ships declarations for this contract: `Plugin` and
|
|
55
|
-
`PluginRuntime` (what `onActivate` receives) from `@jimhoyd/urlcode/plugins`, with
|
|
56
|
-
`PolicyRequest`, `HandlerResult`, `HeaderPair`, `TargetName` and `TestPlan`
|
|
57
|
-
re-exported beside them, and `HostPlugin` (the same type) from `urlcode`. The
|
|
58
|
-
same plugin in TypeScript:
|
|
59
|
-
|
|
60
|
-
```ts
|
|
61
|
-
import type { Plugin, PolicyRequest, HandlerResult } from '@jimhoyd/urlcode/plugins';
|
|
62
|
-
|
|
63
|
-
const auditPlugin: Plugin = {
|
|
64
|
-
name: 'audit',
|
|
65
|
-
version: '1.0.0',
|
|
66
|
-
targets: ['node', 'vercel'],
|
|
67
|
-
onRequest(request: PolicyRequest): HandlerResult | undefined {
|
|
68
|
-
if (request.path === '/deny') return { status: 451, headers: [], body: new Uint8Array(0) };
|
|
69
|
-
return undefined;
|
|
70
|
-
},
|
|
71
|
-
onResponse(request, result) { return { ...result, headers: [...result.headers, ['x-plugin', 'seen']] }; },
|
|
72
|
-
};
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Validation happens at activation, before any request: at most 32 plugins,
|
|
76
|
-
each an object with a kebab-case `name` no other plugin uses, a `version`
|
|
77
|
-
string and a `targets` array of known target names; every declared hook must
|
|
78
|
-
be a function and at least one must be present. A plugin whose `targets` does
|
|
79
|
-
not include the runtime's target is refused with
|
|
80
|
-
`Plugin "name" does not support the vercel target`, mirroring how adapters
|
|
81
|
-
refuse routes, so the same operator application fails fast where a plugin's
|
|
82
|
-
assumptions do not hold.
|
|
83
|
-
|
|
84
|
-
`onActivate(runtime)` receives `{ testPlan(), version, root, target }`.
|
|
85
|
-
`testPlan()` returns the route inventory (`inventory[].policies` names the
|
|
86
|
-
effective policies per route) and `policies`, the per-route policy summaries
|
|
87
|
-
with their `target` value, so a plugin can inspect what it is joining and
|
|
88
|
-
throw to refuse. Hooks may be async; the runtime awaits them.
|
|
89
|
-
|
|
90
|
-
## The request object
|
|
91
|
-
|
|
92
|
-
Every request hook receives the same object the policies see, built once per
|
|
93
|
-
request by `policyRequest` in `src/policies.ts`:
|
|
94
|
-
|
|
95
|
-
| Field | Value |
|
|
96
|
-
|---|---|
|
|
97
|
-
| `method` | Request method, upper case |
|
|
98
|
-
| `target` | Raw request target (path plus query string) as received |
|
|
99
|
-
| `path` | Parsed path |
|
|
100
|
-
| `params` | Route parameter values by name |
|
|
101
|
-
| `query` | Parsed query as `URLSearchParams` |
|
|
102
|
-
| `headers` | Request `Headers` |
|
|
103
|
-
| `headerCounts` | Occurrences per header name, lower case, so a duplicated scalar header can be detected (empty on Cloudflare, where the platform joins repeats) |
|
|
104
|
-
| `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 |
|
|
105
|
-
| `origin` | Public origin the runtime was told (`--origin`, `URLCODE_ORIGIN` or the platform's), used for HSTS and absolute URLs |
|
|
106
|
-
| `route` | The matched route's configured pattern, never request text |
|
|
107
|
-
| `secrets` | `true` when the route declares secret bindings |
|
|
108
|
-
|
|
109
|
-
The object identity is stable for the life of the request, so a plugin may key
|
|
110
|
-
a `WeakMap` on it to carry state from `onRequest` to `onResponse`. Nothing on
|
|
111
|
-
it reaches the guest: there is no sandbox handle, no deadline, no `env` or
|
|
112
|
-
`secrets` values and no binding. A plugin cannot extend or shorten a function's
|
|
113
|
-
deadline, read or write guest state, or obtain a binding the operator policy
|
|
114
|
-
did not grant to the route.
|
|
115
|
-
|
|
116
|
-
## Withholding credential headers from application code
|
|
117
|
-
|
|
118
|
-
An operator plugin may declare `credentialHeaders: ['Cookie', 'Authorization']`.
|
|
119
|
-
Each list contains at most 64 unique HTTP header names, compared
|
|
120
|
-
case-insensitively, each at most 128 characters. The runtime validates the lists
|
|
121
|
-
and captures their union before activation hooks run. Later mutation of a plugin
|
|
122
|
-
object does not weaken the running boundary; a reload captures a new declaration.
|
|
123
|
-
No YAML key can enable, disable or override this operator setting.
|
|
124
|
-
|
|
125
|
-
The union applies to every matched route in that runtime, including public
|
|
126
|
-
functions and guest middleware on native routes. Before processing application
|
|
127
|
-
inputs, the runtime makes a separate header copy and removes those names. Guest
|
|
128
|
-
`Request.headers`, `ctx.inputs.header` and header-derived `ctx.args` receive no
|
|
129
|
-
corresponding values. Header schema defaults are also omitted from the guest
|
|
130
|
-
context for those names. A required protected header input without a default fails with
|
|
131
|
-
`Missing required parameter`, even when the original request supplied it;
|
|
132
|
-
projects should not declare credential inputs that the operator withholds.
|
|
133
|
-
Native redirect query mappings also use this sanitized application context, so
|
|
134
|
-
they cannot reflect a withheld header into a response visible to middleware.
|
|
135
|
-
Selecting `Cookie` removes the entire header, not just one named cookie.
|
|
136
|
-
|
|
137
|
-
Host request/response/error hooks, first-party policies and request-body checks
|
|
138
|
-
retain the original headers. The runtime does not mutate the caller's Headers
|
|
139
|
-
object. Existing applications behave exactly as before when no plugin declares
|
|
140
|
-
credential headers. Plugins still need at least one lifecycle or request hook;
|
|
141
|
-
a header list alone is not a complete plugin.
|
|
142
|
-
|
|
143
|
-
This is a data boundary, not authentication, authorization or cache protection.
|
|
144
|
-
It does not create a principal, validate a token, protect a route, or prevent
|
|
145
|
-
shared caching. In particular, withholding Cookie or Authorization from guests
|
|
146
|
-
does not make a personalized response safe to cache. Operators must separately
|
|
147
|
-
configure the appropriate authentication and cache behavior. It also does not
|
|
148
|
-
redact URL/query/body content, explicit binding grants, logs or values deliberately
|
|
149
|
-
reflected by trusted host plugins. Host plugins retain full access to the request
|
|
150
|
-
and remain trusted operator code.
|
|
151
|
-
|
|
152
|
-
## Results
|
|
153
|
-
|
|
154
|
-
A result is what the runtime writes:
|
|
155
|
-
|
|
156
|
-
```js
|
|
157
|
-
{ status: 429, headers: [['content-type', 'text/plain; charset=utf-8'], ['retry-after', '30']], body: Buffer.from('Too many requests\n') }
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
`headers` is an array of `[name, value]` pairs; `body` is a `Buffer`
|
|
161
|
-
(a `Uint8Array` on Cloudflare). `onRequest` returns such a result to answer the
|
|
162
|
-
request without running the handler, or `undefined` to continue. `onResponse`
|
|
163
|
-
returns the result to send; returning `undefined` keeps the one it was given.
|
|
164
|
-
Replace rather than mutate. Every result still passes through the shared
|
|
165
|
-
response writer on the way out: the status must be an integer from 200 to 599,
|
|
166
|
-
header names and values are validated (an invalid one fails the request as a
|
|
167
|
-
function's would), hop-by-hop headers and `Content-Length` are dropped and
|
|
168
|
-
recomputed, `X-Request-Id` and `X-Content-Type-Options: nosniff` are added, and
|
|
169
|
-
`Cache-Control: no-store` is added when the result carries no `Cache-Control`.
|
|
170
|
-
HEAD and 204/205/304 results are sent without a body.
|
|
171
|
-
|
|
172
|
-
## Ordering
|
|
173
|
-
|
|
174
|
-
Plugins wrap everything else. On the request side the first plugin in the
|
|
175
|
-
array runs first, then the second, then the policies (`agents`, `throttle`,
|
|
176
|
-
`cache`), then the route contract and the handler. On the response side the
|
|
177
|
-
policies run, then plugins in reverse: the first plugin sees the request first
|
|
178
|
-
and the response last, the standard onion. A short-circuit from any
|
|
179
|
-
`onRequest` skips the remaining plugins, every policy request hook and the
|
|
180
|
-
handler, and goes straight to the response side; the response hooks of
|
|
181
|
-
policies with a request phase are skipped for it (nothing is cached, no
|
|
182
|
-
rate-limit headers), security headers and compression still apply, and every
|
|
183
|
-
plugin's `onResponse` still runs.
|
|
184
|
-
|
|
185
|
-
`onError` hooks run in reverse order for an error the runtime throws after
|
|
186
|
-
the request object exists (404 for a disabled route is thrown before it; a
|
|
187
|
-
405 is an ordinary result and reaches `onResponse`). They observe only: a
|
|
188
|
-
throw inside `onError` is swallowed and the outcome stands. A policy's error
|
|
189
|
-
hook may answer with a fallback instead; when one does, plugin `onError`
|
|
190
|
-
hooks do not run and plugin `onResponse` hooks see the fallback (no
|
|
191
|
-
first-party policy returns one today). `onClose` runs in reverse order when
|
|
192
|
-
the runtime closes, after the policies have released their state; a throw
|
|
193
|
-
there is ignored.
|
|
194
|
-
|
|
195
|
-
On a reload the same plugin objects are activated again: `onActivate` runs
|
|
196
|
-
for the new runtime before `onClose` runs for the retired one, so plugin
|
|
197
|
-
state persists unless `onClose` discards it. An `onActivate` that throws
|
|
198
|
-
rejects the reload and the old runtime keeps serving.
|
|
199
|
-
|
|
200
|
-
## What a plugin cannot do
|
|
201
|
-
|
|
202
|
-
- Reach inside the guest, extend a deadline or see bindings: the request
|
|
203
|
-
object carries none of those, and there is no other handle.
|
|
204
|
-
- Change a project's routes or policies after activation; `onActivate` can
|
|
205
|
-
refuse, not rewrite.
|
|
206
|
-
- Be selected from YAML. A project cannot require a plugin, and a plugin's
|
|
207
|
-
presence does not change how the project validates.
|
|
208
|
-
- Change a thrown error's status or body from `onError`.
|
|
209
|
-
- Run on Cloudflare: the Worker carries compiled policies only.
|
|
210
|
-
|
|
211
|
-
## Sketches
|
|
212
|
-
|
|
213
|
-
Both examples are sketches, not shipped code: they omit error handling,
|
|
214
|
-
configuration and the store or verifier they depend on.
|
|
215
|
-
|
|
216
|
-
### Shared-store throttle
|
|
217
|
-
|
|
218
|
-
The built-in `throttle` keeps counters per instance. Across replicas an
|
|
219
|
-
operator needs a shared store; a plugin keys it on the resolved client and the
|
|
220
|
-
route pattern, both of which the request object already carries.
|
|
221
|
-
|
|
222
|
-
```js
|
|
223
|
-
// Sketch. `store.increment(key, windowSeconds)` is a fixed-window counter
|
|
224
|
-
// in a shared store and returns the count after increment.
|
|
225
|
-
export function sharedThrottle({ store, quota, window }) {
|
|
226
|
-
return {
|
|
227
|
-
name: 'shared-throttle', version: '0.1.0', targets: ['node', 'vercel', 'aws'],
|
|
228
|
-
async onRequest(req) {
|
|
229
|
-
const key = `${req.route}|${req.client ?? 'shared'}`;
|
|
230
|
-
const used = await store.increment(key, window);
|
|
231
|
-
if (used <= quota) return undefined;
|
|
232
|
-
return {
|
|
233
|
-
status: 429,
|
|
234
|
-
headers: [['content-type', 'text/plain; charset=utf-8'], ['cache-control', 'no-store'], ['retry-after', String(window)]],
|
|
235
|
-
body: Buffer.from('Too many requests\n'),
|
|
236
|
-
};
|
|
237
|
-
},
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
Combine it with `policies.throttle: false` on the routes it covers, or keep
|
|
243
|
-
the built-in policy as a per-instance ceiling underneath it.
|
|
244
|
-
|
|
245
|
-
### Verified-bot allow
|
|
246
|
-
|
|
247
|
-
The `agents` policy matches strings only. Verifying that a request claiming to
|
|
248
|
-
be a search crawler really comes from one (reverse DNS as the major engines
|
|
249
|
-
document, or an HTTP Message Signature per the web-bot-auth drafts) needs a
|
|
250
|
-
network lookup and a cache, so it is a plugin. The sketch answers 403 to a
|
|
251
|
-
claimed crawler whose address does not verify and lets everything else
|
|
252
|
-
continue to the `agents` policy.
|
|
253
|
-
|
|
254
|
-
```js
|
|
255
|
-
// Sketch. `verify(address)` resolves the address back to the crawler's
|
|
256
|
-
// documented domains and caches the answer; it is not part of the runtime.
|
|
257
|
-
export function verifiedBots({ verify, claims = /Googlebot|bingbot/i }) {
|
|
258
|
-
return {
|
|
259
|
-
name: 'verified-bots', version: '0.1.0', targets: ['node'],
|
|
260
|
-
async onRequest(req) {
|
|
261
|
-
const agent = req.headers.get('user-agent') || '';
|
|
262
|
-
if (!claims.test(agent) || !req.client) return undefined;
|
|
263
|
-
if (await verify(req.client)) return undefined;
|
|
264
|
-
return { status: 403, headers: [['content-type', 'text/plain; charset=utf-8'], ['cache-control', 'no-store']], body: Buffer.from('Forbidden\n') };
|
|
265
|
-
},
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
A plugin that logs should follow the policies' rule: record the route pattern
|
|
271
|
-
and the outcome, not the client address or the User-Agent string.
|