@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.
Files changed (214) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +57 -14
  2. package/.claude/skills/urlcode-operations/SKILL.md +4 -0
  3. package/README.md +42 -18
  4. package/SECURITY.md +5 -3
  5. package/dist/BUILD-MANIFEST.json +30 -27
  6. package/dist/agent-context.js +82 -0
  7. package/dist/agents-guide.js +42 -42
  8. package/dist/authoring.js +12 -2
  9. package/dist/body-schema.js +159 -0
  10. package/dist/build-cloudflare.js +2 -0
  11. package/dist/capabilities.js +1 -1
  12. package/dist/cli.js +28 -12
  13. package/dist/config.js +74 -6
  14. package/dist/context.js +4 -6
  15. package/dist/errors.js +3 -1
  16. package/dist/examples.js +1 -1
  17. package/dist/extensions.js +60 -2
  18. package/dist/http-policy.js +19 -4
  19. package/dist/http-response.js +2 -2
  20. package/dist/init-with.js +71 -9
  21. package/dist/mcp.js +15 -2
  22. package/dist/pattern-guard.js +32 -0
  23. package/dist/policies/security.js +0 -0
  24. package/dist/project-tests.js +35 -11
  25. package/dist/readiness.js +184 -32
  26. package/dist/recipes.js +1 -1
  27. package/dist/router.js +17 -0
  28. package/dist/runtime.js +26 -3
  29. package/dist/scaffold.js +0 -0
  30. package/dist/server.js +26 -3
  31. package/dist/site.js +0 -0
  32. package/dist/tooling.js +2 -1
  33. package/dist/types/agent-context.d.ts +44 -0
  34. package/dist/types/authoring.d.ts +3 -1
  35. package/dist/types/body-schema.d.ts +58 -0
  36. package/dist/types/config.d.ts +10 -2
  37. package/dist/types/context.d.ts +1 -1
  38. package/dist/types/errors.d.ts +9 -1
  39. package/dist/types/examples.d.ts +1 -1
  40. package/dist/types/extensions.d.ts +47 -2
  41. package/dist/types/http-policy.d.ts +3 -0
  42. package/dist/types/init-with.d.ts +10 -1
  43. package/dist/types/pattern-guard.d.ts +10 -0
  44. package/dist/types/project-tests.d.ts +9 -0
  45. package/dist/types/readiness.d.ts +68 -0
  46. package/dist/types/recipes.d.ts +1 -1
  47. package/dist/types/runtime.d.ts +4 -0
  48. package/dist/types/server.d.ts +9 -1
  49. package/dist/types/tooling.d.ts +1 -0
  50. package/dist/types/types.d.ts +13 -0
  51. package/dist/types.js +10 -2
  52. package/dist/typescript-authoring.js +5 -3
  53. package/dist/verify-deployment.js +16 -10
  54. package/examples/body-validation/README.md +16 -0
  55. package/examples/body-validation/example.yaml +17 -0
  56. package/examples/body-validation/tests/requests.json +15 -0
  57. package/examples/body-validation/urlcode.yaml +34 -0
  58. package/examples/coverage-waiver/README.md +8 -0
  59. package/examples/coverage-waiver/example.yaml +16 -0
  60. package/examples/coverage-waiver/functions/notes.mjs +2 -0
  61. package/examples/coverage-waiver/tests/requests.json +3 -0
  62. package/examples/coverage-waiver/urlcode.yaml +10 -0
  63. package/examples/data-dir/README.md +39 -0
  64. package/examples/data-dir/data/welcome.txt +1 -0
  65. package/examples/data-dir/example.yaml +22 -0
  66. package/examples/data-dir/functions/note.mjs +18 -0
  67. package/examples/data-dir/tests/requests.json +6 -0
  68. package/examples/data-dir/urlcode.yaml +6 -0
  69. package/examples/lifecycle/README.md +15 -0
  70. package/examples/lifecycle/example.yaml +19 -0
  71. package/examples/lifecycle/functions/notes.mjs +29 -0
  72. package/examples/lifecycle/tests/requests.json +12 -0
  73. package/examples/lifecycle/urlcode.yaml +29 -0
  74. package/examples/not-found/README.md +10 -0
  75. package/examples/not-found/example.yaml +17 -0
  76. package/examples/not-found/public/404.html +3 -0
  77. package/examples/not-found/public/index.html +3 -0
  78. package/examples/not-found/tests/requests.json +7 -0
  79. package/examples/not-found/urlcode.yaml +7 -0
  80. package/examples/shared-blocks/README.md +11 -0
  81. package/examples/shared-blocks/example.yaml +17 -0
  82. package/examples/shared-blocks/tests/requests.json +7 -0
  83. package/examples/shared-blocks/urlcode.yaml +37 -0
  84. package/llms-full.txt +364 -49
  85. package/llms.txt +44 -6
  86. package/package.json +15 -26
  87. package/recipes/static-page/README.md +9 -0
  88. package/recipes/static-page/public/index.html +11 -0
  89. package/recipes/static-page/recipe.yaml +21 -0
  90. package/recipes/static-page/tests/requests.json +22 -0
  91. package/recipes/static-page/urlcode.yaml +7 -0
  92. package/recipes/static-plus-api/README.md +6 -0
  93. package/recipes/static-plus-api/urlcode.yaml +4 -0
  94. package/recipes/store-crud/README.md +53 -0
  95. package/recipes/store-crud/recipe.yaml +31 -0
  96. package/recipes/store-crud/tests/requests.json +18 -0
  97. package/recipes/store-crud/urlcode.yaml +18 -0
  98. package/schemas/urlcode.schema.json +125 -60
  99. package/skills/urlcode/SKILL.md +47 -27
  100. package/starters/default/AGENTS.md +43 -43
  101. package/starters/page/README.md +14 -0
  102. package/starters/page/public/index.html +12 -0
  103. package/starters/page/tests/requests.json +17 -0
  104. package/starters/page/urlcode.yaml +6 -0
  105. package/.claude-plugin/marketplace.json +0 -18
  106. package/CONTRIBUTING.md +0 -148
  107. package/ROADMAP.md +0 -70
  108. package/docs/AI-AUTHORING.md +0 -339
  109. package/docs/ASSETS.md +0 -107
  110. package/docs/AUTH-BACKUP.md +0 -32
  111. package/docs/AWS.md +0 -86
  112. package/docs/BEST-PRACTICES.md +0 -276
  113. package/docs/BULK.md +0 -79
  114. package/docs/CAPABILITIES.md +0 -192
  115. package/docs/CAPACITY.md +0 -305
  116. package/docs/CI-FOLLOWUP-2026-09-19.md +0 -97
  117. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +0 -322
  118. package/docs/CI.md +0 -147
  119. package/docs/CLOUDFLARE.md +0 -109
  120. package/docs/CODEBASE-AUDIT-2026-09-20.md +0 -284
  121. package/docs/COMPLIANCE.md +0 -239
  122. package/docs/COMPOSING-A-SITE.md +0 -287
  123. package/docs/CONDITIONS.md +0 -74
  124. package/docs/CONTAINER-PROMOTION.md +0 -74
  125. package/docs/DEPLOYMENT-CHECKS.md +0 -108
  126. package/docs/DEVELOPMENT-PIPELINE.md +0 -304
  127. package/docs/EGRESS.md +0 -125
  128. package/docs/EXTENSIONS.md +0 -403
  129. package/docs/FRAMEWORK.md +0 -217
  130. package/docs/FUNCTION-SECURITY.md +0 -251
  131. package/docs/HTTP.md +0 -129
  132. package/docs/INSTALL.md +0 -128
  133. package/docs/INTERCHANGE.md +0 -134
  134. package/docs/LOAD-TESTING.md +0 -91
  135. package/docs/LOCAL-DEVELOPMENT.md +0 -102
  136. package/docs/MIDDLEWARE-EXAMPLES.md +0 -75
  137. package/docs/MIDDLEWARE.md +0 -102
  138. package/docs/MONITORING.md +0 -115
  139. package/docs/OBSERVABILITY.md +0 -222
  140. package/docs/OPEN-DECISIONS.md +0 -149
  141. package/docs/OPERATIONAL-PROOF.md +0 -41
  142. package/docs/OPERATIONS.md +0 -201
  143. package/docs/ORGANIZATION.md +0 -135
  144. package/docs/PERFORMANCE.md +0 -72
  145. package/docs/PLUGINS.md +0 -271
  146. package/docs/POLICIES.md +0 -211
  147. package/docs/PRERENDER.md +0 -245
  148. package/docs/PROJECT-DIRECTION.md +0 -118
  149. package/docs/PROVIDER-VERIFICATION.md +0 -84
  150. package/docs/READINESS.md +0 -153
  151. package/docs/README.md +0 -93
  152. package/docs/RECIPES.md +0 -99
  153. package/docs/RELEASE-0.4.0-alpha.3.md +0 -50
  154. package/docs/RELEASE-0.4.1.md +0 -73
  155. package/docs/RELEASE-0.4.2.md +0 -30
  156. package/docs/RELEASE-READINESS.md +0 -146
  157. package/docs/RELEASE-SECURITY.md +0 -100
  158. package/docs/RESILIENCE.md +0 -161
  159. package/docs/ROUTING.md +0 -92
  160. package/docs/SANDBOX-REVIEW.md +0 -72
  161. package/docs/SCAFFOLDING.md +0 -70
  162. package/docs/SECURITY-AUDIT.md +0 -164
  163. package/docs/SITE.md +0 -150
  164. package/docs/SPECIFICATION.md +0 -359
  165. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +0 -288
  166. package/docs/SPIKE-BUSINESS-SUITE.md +0 -1029
  167. package/docs/SPIKE-CORE-LAYERING.md +0 -368
  168. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +0 -207
  169. package/docs/STANDARDS.md +0 -311
  170. package/docs/STARTERS.md +0 -83
  171. package/docs/STATIC.md +0 -105
  172. package/docs/TOOLING.md +0 -298
  173. package/docs/TUNNELS.md +0 -72
  174. package/docs/TYPESCRIPT-AUTHORING.md +0 -87
  175. package/docs/TYPESCRIPT.md +0 -123
  176. package/docs/VERCEL.md +0 -114
  177. package/docs/VERSION-ALIGNMENT.md +0 -88
  178. package/docs/YAML-GUIDE.md +0 -57
  179. package/docs/YAML-REFERENCE.md +0 -449
  180. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +0 -68
  181. package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +0 -102
  182. package/docs/archive/2026-09-19/NEXT-PHASE-PLAN.md +0 -108
  183. package/docs/archive/2026-09-19/NEXT-STEPS.md +0 -646
  184. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +0 -277
  185. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +0 -186
  186. package/docs/archive/2026-09-19/ROADMAP.md +0 -387
  187. package/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md +0 -430
  188. package/docs/archive/2026-09-19/SPIKE-EXTENSIONS.md +0 -492
  189. package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +0 -365
  190. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +0 -778
  191. package/docs/archive/2026-09-19/USABILITY-REVIEW.md +0 -139
  192. package/docs/archive/2026-09-20/OPEN-DECISIONS-COMPLETED.md +0 -116
  193. package/docs/archive/README.md +0 -29
  194. package/docs/policies/agents.md +0 -182
  195. package/docs/policies/cache.md +0 -152
  196. package/docs/policies/compression.md +0 -169
  197. package/docs/policies/contract.md +0 -52
  198. package/docs/policies/hardened.md +0 -56
  199. package/docs/policies/interoperability.md +0 -169
  200. package/docs/policies/operations.md +0 -45
  201. package/docs/policies/security.md +0 -161
  202. package/docs/policies/throttle.md +0 -103
  203. package/docs/yaml/assets.md +0 -36
  204. package/docs/yaml/conditions.md +0 -20
  205. package/docs/yaml/functions.md +0 -168
  206. package/docs/yaml/middleware.md +0 -31
  207. package/docs/yaml/organization.md +0 -74
  208. package/docs/yaml/policies.md +0 -37
  209. package/docs/yaml/redirects.md +0 -64
  210. package/docs/yaml/responses.md +0 -57
  211. package/docs/yaml/site.md +0 -24
  212. package/packaging/claude-plugin/.claude-plugin/plugin.json +0 -19
  213. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +0 -130
  214. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +0 -108
@@ -1,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).
@@ -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.