@jimhoyd/urlcode 0.4.1 → 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.
Files changed (212) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +60 -7
  2. package/.claude/skills/urlcode-operations/SKILL.md +4 -0
  3. package/README.md +19 -15
  4. package/SECURITY.md +5 -3
  5. package/dist/BUILD-MANIFEST.json +31 -28
  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 +29 -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 +138 -3
  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/policy.js +16 -0
  25. package/dist/project-tests.js +35 -11
  26. package/dist/readiness.js +188 -32
  27. package/dist/recipes.js +1 -1
  28. package/dist/router.js +17 -0
  29. package/dist/runtime.js +26 -3
  30. package/dist/scaffold.js +0 -0
  31. package/dist/server.js +26 -3
  32. package/dist/site.js +0 -0
  33. package/dist/tooling.js +3 -1
  34. package/dist/types/agent-context.d.ts +44 -0
  35. package/dist/types/authoring.d.ts +3 -1
  36. package/dist/types/body-schema.d.ts +58 -0
  37. package/dist/types/config.d.ts +10 -2
  38. package/dist/types/context.d.ts +1 -1
  39. package/dist/types/errors.d.ts +9 -1
  40. package/dist/types/examples.d.ts +1 -1
  41. package/dist/types/extensions.d.ts +105 -2
  42. package/dist/types/http-policy.d.ts +3 -0
  43. package/dist/types/init-with.d.ts +10 -1
  44. package/dist/types/pattern-guard.d.ts +10 -0
  45. package/dist/types/project-tests.d.ts +9 -0
  46. package/dist/types/readiness.d.ts +71 -0
  47. package/dist/types/recipes.d.ts +1 -1
  48. package/dist/types/runtime.d.ts +4 -0
  49. package/dist/types/server.d.ts +9 -1
  50. package/dist/types/tooling.d.ts +2 -0
  51. package/dist/types/types.d.ts +13 -0
  52. package/dist/types.js +10 -2
  53. package/dist/typescript-authoring.js +5 -3
  54. package/dist/verify-deployment.js +16 -10
  55. package/examples/body-validation/README.md +16 -0
  56. package/examples/body-validation/example.yaml +17 -0
  57. package/examples/body-validation/tests/requests.json +15 -0
  58. package/examples/body-validation/urlcode.yaml +34 -0
  59. package/examples/coverage-waiver/README.md +8 -0
  60. package/examples/coverage-waiver/example.yaml +16 -0
  61. package/examples/coverage-waiver/functions/notes.mjs +2 -0
  62. package/examples/coverage-waiver/tests/requests.json +3 -0
  63. package/examples/coverage-waiver/urlcode.yaml +10 -0
  64. package/examples/data-dir/README.md +39 -0
  65. package/examples/data-dir/data/welcome.txt +1 -0
  66. package/examples/data-dir/example.yaml +22 -0
  67. package/examples/data-dir/functions/note.mjs +18 -0
  68. package/examples/data-dir/tests/requests.json +6 -0
  69. package/examples/data-dir/urlcode.yaml +6 -0
  70. package/examples/lifecycle/README.md +15 -0
  71. package/examples/lifecycle/example.yaml +19 -0
  72. package/examples/lifecycle/functions/notes.mjs +29 -0
  73. package/examples/lifecycle/tests/requests.json +12 -0
  74. package/examples/lifecycle/urlcode.yaml +29 -0
  75. package/examples/not-found/README.md +10 -0
  76. package/examples/not-found/example.yaml +17 -0
  77. package/examples/not-found/public/404.html +3 -0
  78. package/examples/not-found/public/index.html +3 -0
  79. package/examples/not-found/tests/requests.json +7 -0
  80. package/examples/not-found/urlcode.yaml +7 -0
  81. package/examples/shared-blocks/README.md +11 -0
  82. package/examples/shared-blocks/example.yaml +17 -0
  83. package/examples/shared-blocks/tests/requests.json +7 -0
  84. package/examples/shared-blocks/urlcode.yaml +37 -0
  85. package/llms-full.txt +436 -149
  86. package/llms.txt +44 -6
  87. package/package.json +15 -22
  88. package/recipes/static-page/README.md +9 -0
  89. package/recipes/static-page/public/index.html +11 -0
  90. package/recipes/static-page/recipe.yaml +21 -0
  91. package/recipes/static-page/tests/requests.json +22 -0
  92. package/recipes/static-page/urlcode.yaml +7 -0
  93. package/recipes/static-plus-api/README.md +6 -0
  94. package/recipes/static-plus-api/urlcode.yaml +4 -0
  95. package/recipes/store-crud/README.md +53 -0
  96. package/recipes/store-crud/recipe.yaml +31 -0
  97. package/recipes/store-crud/tests/requests.json +18 -0
  98. package/recipes/store-crud/urlcode.yaml +18 -0
  99. package/schemas/urlcode.schema.json +125 -60
  100. package/skills/urlcode/SKILL.md +53 -26
  101. package/starters/default/AGENTS.md +43 -43
  102. package/starters/page/README.md +14 -0
  103. package/starters/page/public/index.html +12 -0
  104. package/starters/page/tests/requests.json +17 -0
  105. package/starters/page/urlcode.yaml +6 -0
  106. package/.claude-plugin/marketplace.json +0 -18
  107. package/CONTRIBUTING.md +0 -112
  108. package/ROADMAP.md +0 -66
  109. package/docs/AI-AUTHORING.md +0 -338
  110. package/docs/ASSETS.md +0 -107
  111. package/docs/AUTH-BACKUP.md +0 -32
  112. package/docs/AWS.md +0 -86
  113. package/docs/BEST-PRACTICES.md +0 -276
  114. package/docs/BULK.md +0 -79
  115. package/docs/CAPABILITIES.md +0 -192
  116. package/docs/CAPACITY.md +0 -305
  117. package/docs/CI-FOLLOWUP-2026-09-19.md +0 -97
  118. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +0 -322
  119. package/docs/CI.md +0 -147
  120. package/docs/CLOUDFLARE.md +0 -109
  121. package/docs/CODEBASE-AUDIT-2026-09-20.md +0 -284
  122. package/docs/COMPLIANCE.md +0 -239
  123. package/docs/COMPOSING-A-SITE.md +0 -278
  124. package/docs/CONDITIONS.md +0 -74
  125. package/docs/DEPLOYMENT-CHECKS.md +0 -108
  126. package/docs/DEVELOPMENT-PIPELINE.md +0 -270
  127. package/docs/EGRESS.md +0 -125
  128. package/docs/EXTENSIONS.md +0 -438
  129. package/docs/FRAMEWORK.md +0 -217
  130. package/docs/FUNCTION-SECURITY.md +0 -254
  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 -224
  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 -150
  151. package/docs/README.md +0 -87
  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-READINESS.md +0 -117
  156. package/docs/RELEASE-SECURITY.md +0 -96
  157. package/docs/RESILIENCE.md +0 -161
  158. package/docs/ROUTING.md +0 -92
  159. package/docs/SANDBOX-REVIEW.md +0 -72
  160. package/docs/SCAFFOLDING.md +0 -70
  161. package/docs/SECURITY-AUDIT.md +0 -164
  162. package/docs/SITE.md +0 -150
  163. package/docs/SPECIFICATION.md +0 -359
  164. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +0 -288
  165. package/docs/SPIKE-BUSINESS-SUITE.md +0 -1029
  166. package/docs/SPIKE-CORE-LAYERING.md +0 -368
  167. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +0 -211
  168. package/docs/STANDARDS.md +0 -311
  169. package/docs/STARTERS.md +0 -83
  170. package/docs/STATIC.md +0 -105
  171. package/docs/TOOLING.md +0 -298
  172. package/docs/TUNNELS.md +0 -72
  173. package/docs/TYPESCRIPT-AUTHORING.md +0 -87
  174. package/docs/TYPESCRIPT.md +0 -123
  175. package/docs/VERCEL.md +0 -114
  176. package/docs/VERSION-ALIGNMENT.md +0 -80
  177. package/docs/YAML-GUIDE.md +0 -57
  178. package/docs/YAML-REFERENCE.md +0 -449
  179. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +0 -68
  180. package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +0 -102
  181. package/docs/archive/2026-09-19/NEXT-PHASE-PLAN.md +0 -108
  182. package/docs/archive/2026-09-19/NEXT-STEPS.md +0 -646
  183. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +0 -277
  184. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +0 -186
  185. package/docs/archive/2026-09-19/ROADMAP.md +0 -387
  186. package/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md +0 -430
  187. package/docs/archive/2026-09-19/SPIKE-EXTENSIONS.md +0 -492
  188. package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +0 -365
  189. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +0 -778
  190. package/docs/archive/2026-09-19/USABILITY-REVIEW.md +0 -139
  191. package/docs/archive/README.md +0 -28
  192. package/docs/policies/agents.md +0 -182
  193. package/docs/policies/cache.md +0 -152
  194. package/docs/policies/compression.md +0 -169
  195. package/docs/policies/contract.md +0 -52
  196. package/docs/policies/hardened.md +0 -56
  197. package/docs/policies/interoperability.md +0 -169
  198. package/docs/policies/operations.md +0 -45
  199. package/docs/policies/security.md +0 -161
  200. package/docs/policies/throttle.md +0 -103
  201. package/docs/yaml/assets.md +0 -36
  202. package/docs/yaml/conditions.md +0 -20
  203. package/docs/yaml/functions.md +0 -168
  204. package/docs/yaml/middleware.md +0 -31
  205. package/docs/yaml/organization.md +0 -74
  206. package/docs/yaml/policies.md +0 -37
  207. package/docs/yaml/redirects.md +0 -64
  208. package/docs/yaml/responses.md +0 -57
  209. package/docs/yaml/site.md +0 -24
  210. package/packaging/claude-plugin/.claude-plugin/plugin.json +0 -19
  211. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +0 -120
  212. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +0 -108
@@ -1,201 +0,0 @@
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.3.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.3.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. Core has no writable
49
- mount of its own; a future mount-based extension (like `auth`/`admin`, see
50
- [extensions](EXTENSIONS.md)) is the place for operator-owned writable state.
51
- Sandboxed application functions cannot access mounted files or installed
52
- Node packages. The resource values above illustrate
53
- container limits, not a sizing recommendation; large configuration compilation
54
- can need more memory. Measure your workload. Tag/redeploy immutable image digests
55
- in real operation rather than treating a mutable tag as a rollback identity.
56
-
57
- ## Domains, HTTPS and exposure
58
-
59
- Point your domain's DNS at the reverse proxy/load balancer you operate, terminate
60
- HTTPS there, and forward to the loopback/private URLCode port. Use a tested proxy
61
- such as your existing Caddy/nginx/load-balancer setup for certificates, connection
62
- limits and rate limiting. No certificate automation is supplied by URLCode yet.
63
- Keep direct backend access private. Restrict `/_urlcode/*` endpoints to operators
64
- at the proxy; they are unauthenticated and reveal route count/config digest.
65
-
66
- If functions perform sensitive actions, implement authentication and authorization
67
- in the application. A short URL is not automatically an access-control mechanism.
68
- Functions and middleware run trusted and unsandboxed by default, in the host
69
- process with full Node, filesystem and network access; a route that declares
70
- `sandbox: true` runs isolated in QuickJS/WebAssembly instead (see
71
- [function security](FUNCTION-SECURITY.md)). Keep separate deployment
72
- processes/containers and narrowly scoped credentials as additional boundaries.
73
- Do not expose a public code-upload/multi-tenant service on the basis of the self-hosted release alone
74
- without separate security review and stronger service-level containment.
75
-
76
- ## Secrets and rotation
77
-
78
- `dev`, `test`, `routes`, `audit`, `benchmark` and `validate --local` read
79
- `.env.local`. Authoring and
80
- permissions inspection do not read credentials or execute functions. `serve` and
81
- ordinary `validate` use process environment only. Resolve logical names from
82
- your own secret store/supervisor and inject them at startup; direct provider
83
- secret-store integrations remain future work. Bindings also require an external,
84
- revision-pinned operator policy; see [setup](FUNCTION-SECURITY.md). Do not place secret values in
85
- command-line arguments, route YAML, image layers or Git.
86
-
87
- Check tracked files as well as ignore rules. Docker builds use an explicit
88
- allowlist; npm artifacts include runtime/schema/starter/docs files only. Do not
89
- build an application image by blindly copying its entire development directory.
90
- Rotate a credential by replacing its injected value and restarting/redeploying;
91
- production does not watch or refresh secret values automatically.
92
-
93
- ## Health, logs and limits
94
-
95
- - `GET /_urlcode/health`: process liveness.
96
- - `GET /_urlcode/ready`: 200 when the active snapshot and all function workers
97
- are available; 503 while a worker
98
- is unavailable. Busy workers alone do not
99
- mark readiness down. A failed worker is replaced with
100
- exponential backoff (250 ms doubling to a 30-second ceiling) and readiness
101
- reports 503 until every slot is serving again. Replacement does not stop, so a
102
- request-triggered deadline cannot disable functions until an operator restarts;
103
- a cause that keeps recurring keeps the instance shedding load and needs an
104
- operator. Alert on sustained `function_worker` restart events.
105
- - Probes are answered from their own admission budget (16 by default,
106
- `--max-in-flight-health`), so they stay available while the application is
107
- saturated without being an unmetered endpoint. They are unauthenticated and
108
- report the configuration digest and route count: keep them on an internal
109
- interface or restrict them at the ingress.
110
- - Request logs: JSON request ID, status and duration. No URLs, query strings,
111
- headers, bodies, bindings or user exception text. `--request-log detailed` adds
112
- the request method and the matched route pattern (`/u/{id}`, or `null` when
113
- nothing matched). Both come from the reviewed configuration, never from
114
- request-supplied path, parameter or query text, which is what makes per-route
115
- error rates and latency available without logging user data.
116
- - Request IDs are generated per request and returned in `x-request-id`. An
117
- inbound `x-request-id` is ignored unless `--trust-request-id` is set, which is
118
- only correct when a trusted proxy sets the header and strips client-supplied
119
- copies; untrusted values are still rejected unless they are a single header of
120
- at most 128 characters from `[A-Za-z0-9_.:-]`. Forward stdout to your log
121
- system and alert on sustained 5xx and latency. The default logger drops records
122
- when stdout buffering reaches 1 MiB and reports the dropped count when output
123
- recovers; alert on `logs_dropped`. Function console output is
124
- suppressed; app-specific diagnostics are not yet a first-class feature.
125
- Synchronous and asynchronous sink failures are contained; a failed sink drops
126
- subsequent output and needs operator recovery. Collectors own rotation/retention.
127
- - HTTP: 8,192-character target, 16 KiB headers, 1 MiB buffered body, 15-second request
128
- receipt timeout, 10-second header timeout, 5-second keep-alive, 1,000 requests
129
- per socket and 1,024 active connections. At most 64 application requests are
130
- admitted through response completion; excess requests receive 503. Health probes
131
- remain available under admission saturation. A 15-second socket inactivity
132
- timeout closes stalled readers/writers. Proxy timeouts/rate limits still matter.
133
- - Functions: a `sandbox: true` route gets 2 concurrent workers (`--workers`),
134
- no queue and a 5-second deadline (`--function-timeout-ms`); a trusted route
135
- (`sandbox` false or absent, the default) shares the in-flight admission cap
136
- instead of a worker pool and races the same deadline. Either mode buffers
137
- 1 MiB of response (`--max-response-bytes`) and 16 KiB response headers.
138
- Saturation 503; timeout 504; error 502.
139
- QuickJS guests have a 32 MiB heap and 512 KiB stack budget and no network or
140
- host capabilities; a trusted route has neither budget and full Node access.
141
- Outer workers have additional V8 limits. Total process/WASM
142
- memory still needs deployment-level limits; do not equate guest budget with RSS.
143
-
144
- `urlcode serve`/`dev` and the JavaScript server API both configure workers,
145
- deadlines and byte limits: `--workers`, `--function-timeout-ms`,
146
- `--max-response-bytes`, `--max-body-bytes`, `--max-in-flight` and
147
- `--max-in-flight-health`. Set them on the container command line; these are
148
- deployment controls, not portable route behavior. Horizontal replicas
149
- must use identical application/config versions and secret bindings. In-memory
150
- function state is reset after every invocation, not durable/shared application state.
151
- General application storage needs a future explicit capability broker; no
152
- storage/network access is exposed to the guest. Core no longer has a native
153
- link store, and the `urlcode-dynamic-link` extension package that replaced it
154
- has been retired and unpublished.
155
-
156
- The health version combines route-definition and asset-representation digests;
157
- it does not identify the complete function/runtime release. Record runtime commit,
158
- application commit, dependency locks and image digest in your deployment system.
159
-
160
- ## Deployment and rollback procedure
161
-
162
- 1. Build a candidate from pinned runtime/application revisions and lockfiles.
163
- 2. Validate its config/bindings and run local HTTP tests without external redirects.
164
- 3. Start it on an alternate private port/container. Check readiness and representative
165
- redirect/function behavior through the intended proxy configuration:
166
- `urlcode verify-deployment --project . --target https://candidate.host` compares
167
- version, fixtures, policy headers and site files with the project
168
- ([deployment checks](DEPLOYMENT-CHECKS.md)).
169
- 4. Switch proxy traffic after checks pass. Drain the previous instance before stopping.
170
- 5. If checks or observed behavior fail, route traffic back to the retained previous
171
- instance/image and its compatible secret bindings.
172
-
173
- This is an operator procedure, not an implemented deployment control plane.
174
- Rollback cannot undo a function's external side effects or migrate an app's
175
- state automatically. Plan those independently. Keep Git definitions backed up;
176
- back up any app-owned persistent state separately. YAML routes require no database.
177
-
178
- ## Capacity and incident planning
179
-
180
- See [capacity and concurrency](CAPACITY.md) for hard limits, worker occupancy,
181
- no-queue rejection, memory/reload budgets and theoretical sizing. See
182
- [DDoS and recovery](RESILIENCE.md) for ingress responsibilities, incident response,
183
- rollback/restore procedures, recovery objectives and drills.
184
-
185
- See the [release-readiness register](RELEASE-READINESS.md) for evidence and open gates.
186
-
187
- ## Remaining production validation
188
-
189
- Before approving a production deployment: run sustained soak/load tests on its
190
- hardware, obtain independent security review, exercise failure/restart and
191
- upgrade/rollback, establish a clear support/reporting policy, and add the needed
192
- operational metrics. Provider adapters remain separate roadmap work. See
193
- [roadmap](../ROADMAP.md). No claim of high
194
- availability, zero downtime or provider portability beyond the Node process
195
- adapter is made by the current release.
196
-
197
- ## Security review
198
-
199
- The [2026-09-16 internal audit](SECURITY-AUDIT.md) records fixes, regression evidence
200
- and remaining security/operational gates. This is not an independent assessment.
201
-
@@ -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.