@jimhoyd/urlcode 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/CONTRIBUTING.md +80 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +20 -0
  4. package/README.md +342 -0
  5. package/ROADMAP.md +248 -0
  6. package/SECURITY.md +40 -0
  7. package/data/agents/LICENSES/ai-robots-txt.txt +24 -0
  8. package/data/agents/LICENSES/crawler-user-agents.txt +23 -0
  9. package/data/agents/ai-crawlers.json +1240 -0
  10. package/data/agents/crawlers.json +10515 -0
  11. package/data/agents/index.d.ts +3 -0
  12. package/data/agents/index.js +2453 -0
  13. package/data/agents/monitoring.json +1520 -0
  14. package/data/agents/seo.json +3865 -0
  15. package/dist/BUILD-MANIFEST.json +60 -0
  16. package/dist/adapters.js +59 -0
  17. package/dist/agent-lists.js +59 -0
  18. package/dist/assets.js +129 -0
  19. package/dist/authoring.js +68 -0
  20. package/dist/aws.js +110 -0
  21. package/dist/build-cloudflare.js +164 -0
  22. package/dist/cli.js +185 -0
  23. package/dist/client-address.js +85 -0
  24. package/dist/cloudflare.js +165 -0
  25. package/dist/compliance-rules/baseline.js +98 -0
  26. package/dist/compliance-rules/privacy.js +38 -0
  27. package/dist/compliance-rules/shared.js +62 -0
  28. package/dist/compliance-rules/strict.js +61 -0
  29. package/dist/compliance.js +219 -0
  30. package/dist/config-worker.js +9 -0
  31. package/dist/config.js +172 -0
  32. package/dist/errors.js +8 -0
  33. package/dist/function-sources.js +54 -0
  34. package/dist/function-worker.js +105 -0
  35. package/dist/functions.js +174 -0
  36. package/dist/guest-api.js +114 -0
  37. package/dist/header-validation.js +18 -0
  38. package/dist/http-policy.js +74 -0
  39. package/dist/http-response.js +84 -0
  40. package/dist/index.js +11 -0
  41. package/dist/link-api.js +136 -0
  42. package/dist/link-cli.js +141 -0
  43. package/dist/link-events.js +76 -0
  44. package/dist/link-records.js +31 -0
  45. package/dist/link-store-worker.js +150 -0
  46. package/dist/link-store.js +250 -0
  47. package/dist/logging.js +22 -0
  48. package/dist/management-policy.js +41 -0
  49. package/dist/match.js +124 -0
  50. package/dist/observability.js +242 -0
  51. package/dist/plugins.js +74 -0
  52. package/dist/policies/agents.js +248 -0
  53. package/dist/policies/cache.js +297 -0
  54. package/dist/policies/compression.js +187 -0
  55. package/dist/policies/security.js +0 -0
  56. package/dist/policies/throttle.js +131 -0
  57. package/dist/policies.js +142 -0
  58. package/dist/policy.js +55 -0
  59. package/dist/prerender.js +165 -0
  60. package/dist/project-tests.js +23 -0
  61. package/dist/readiness.js +216 -0
  62. package/dist/router.js +166 -0
  63. package/dist/runtime.js +244 -0
  64. package/dist/scaffold.js +0 -0
  65. package/dist/scripts/operational-drills.js +75 -0
  66. package/dist/server.js +270 -0
  67. package/dist/site.js +0 -0
  68. package/dist/sqlite-version.js +6 -0
  69. package/dist/types/adapters.d.ts +12 -0
  70. package/dist/types/agent-lists.d.ts +5 -0
  71. package/dist/types/assets.d.ts +13 -0
  72. package/dist/types/authoring.d.ts +2 -0
  73. package/dist/types/aws.d.ts +35 -0
  74. package/dist/types/build-cloudflare.d.ts +14 -0
  75. package/dist/types/cli.d.ts +2 -0
  76. package/dist/types/client-address.d.ts +9 -0
  77. package/dist/types/cloudflare.d.ts +75 -0
  78. package/dist/types/compliance-rules/baseline.d.ts +12 -0
  79. package/dist/types/compliance-rules/privacy.d.ts +6 -0
  80. package/dist/types/compliance-rules/shared.d.ts +24 -0
  81. package/dist/types/compliance-rules/strict.d.ts +9 -0
  82. package/dist/types/compliance.d.ts +119 -0
  83. package/dist/types/config-worker.d.ts +1 -0
  84. package/dist/types/config.d.ts +21 -0
  85. package/dist/types/errors.d.ts +7 -0
  86. package/dist/types/function-sources.d.ts +21 -0
  87. package/dist/types/function-worker.d.ts +1 -0
  88. package/dist/types/functions.d.ts +92 -0
  89. package/dist/types/guest-api.d.ts +16 -0
  90. package/dist/types/header-validation.d.ts +2 -0
  91. package/dist/types/http-policy.d.ts +39 -0
  92. package/dist/types/http-response.d.ts +43 -0
  93. package/dist/types/index.d.ts +11 -0
  94. package/dist/types/link-api.d.ts +30 -0
  95. package/dist/types/link-cli.d.ts +36 -0
  96. package/dist/types/link-events.d.ts +27 -0
  97. package/dist/types/link-records.d.ts +11 -0
  98. package/dist/types/link-store-worker.d.ts +1 -0
  99. package/dist/types/link-store.d.ts +130 -0
  100. package/dist/types/logging.d.ts +9 -0
  101. package/dist/types/management-policy.d.ts +9 -0
  102. package/dist/types/match.d.ts +81 -0
  103. package/dist/types/observability.d.ts +104 -0
  104. package/dist/types/plugins.d.ts +27 -0
  105. package/dist/types/policies/agents.d.ts +86 -0
  106. package/dist/types/policies/cache.d.ts +82 -0
  107. package/dist/types/policies/compression.d.ts +41 -0
  108. package/dist/types/policies/security.d.ts +37 -0
  109. package/dist/types/policies/throttle.d.ts +55 -0
  110. package/dist/types/policies.d.ts +36 -0
  111. package/dist/types/policy.d.ts +21 -0
  112. package/dist/types/prerender.d.ts +31 -0
  113. package/dist/types/project-tests.d.ts +13 -0
  114. package/dist/types/readiness.d.ts +121 -0
  115. package/dist/types/router.d.ts +11 -0
  116. package/dist/types/runtime.d.ts +91 -0
  117. package/dist/types/scaffold.d.ts +17 -0
  118. package/dist/types/server.d.ts +37 -0
  119. package/dist/types/site.d.ts +12 -0
  120. package/dist/types/sqlite-version.d.ts +1 -0
  121. package/dist/types/types.d.ts +332 -0
  122. package/dist/types/vercel.d.ts +12 -0
  123. package/dist/types.js +177 -0
  124. package/dist/vercel.js +65 -0
  125. package/docs/AI-AUTHORING.md +113 -0
  126. package/docs/ASSETS.md +106 -0
  127. package/docs/AWS.md +77 -0
  128. package/docs/BEST-PRACTICES.md +267 -0
  129. package/docs/CAPACITY.md +208 -0
  130. package/docs/CLOUDFLARE.md +110 -0
  131. package/docs/COMPLIANCE.md +242 -0
  132. package/docs/DYNAMIC-LINKS.md +561 -0
  133. package/docs/FUNCTION-SECURITY.md +113 -0
  134. package/docs/HTTP.md +129 -0
  135. package/docs/INSTALL.md +90 -0
  136. package/docs/LOAD-TESTING.md +91 -0
  137. package/docs/LOCAL-DEVELOPMENT.md +99 -0
  138. package/docs/MANAGEMENT-SECURITY.md +82 -0
  139. package/docs/MIDDLEWARE.md +83 -0
  140. package/docs/MONITORING.md +132 -0
  141. package/docs/OBSERVABILITY.md +229 -0
  142. package/docs/OPERATIONAL-PROOF.md +42 -0
  143. package/docs/OPERATIONS.md +207 -0
  144. package/docs/ORGANIZATION.md +135 -0
  145. package/docs/PERFORMANCE.md +72 -0
  146. package/docs/PLUGINS.md +234 -0
  147. package/docs/POLICIES.md +489 -0
  148. package/docs/PRERENDER.md +193 -0
  149. package/docs/PROJECT-DIRECTION.md +51 -0
  150. package/docs/READINESS.md +125 -0
  151. package/docs/README.md +58 -0
  152. package/docs/RELEASE-READINESS.md +95 -0
  153. package/docs/RELEASE-SECURITY.md +72 -0
  154. package/docs/RESILIENCE.md +160 -0
  155. package/docs/ROUTING.md +95 -0
  156. package/docs/SANDBOX-REVIEW.md +59 -0
  157. package/docs/SCAFFOLDING.md +72 -0
  158. package/docs/SECURITY-AUDIT.md +124 -0
  159. package/docs/SITE.md +150 -0
  160. package/docs/SPECIFICATION.md +232 -0
  161. package/docs/SPIKE-EXTENSIONS.md +475 -0
  162. package/docs/STANDARDS.md +303 -0
  163. package/docs/STARTERS.md +51 -0
  164. package/docs/TUNNELS.md +75 -0
  165. package/docs/TYPESCRIPT.md +102 -0
  166. package/docs/VERCEL.md +107 -0
  167. package/docs/YAML-GUIDE.md +521 -0
  168. package/docs/YAML-REFERENCE.md +328 -0
  169. package/docs/policies/agents.md +182 -0
  170. package/docs/policies/cache.md +139 -0
  171. package/docs/policies/compression.md +168 -0
  172. package/docs/policies/security.md +160 -0
  173. package/docs/policies/throttle.md +103 -0
  174. package/examples/assets/.env.example +1 -0
  175. package/examples/assets/.gitattributes +2 -0
  176. package/examples/assets/Makefile +30 -0
  177. package/examples/assets/README.md +18 -0
  178. package/examples/assets/functions/hello.mjs +3 -0
  179. package/examples/assets/gitignore.template +7 -0
  180. package/examples/assets/public/about.html +2 -0
  181. package/examples/assets/public/assets/example.txt +1 -0
  182. package/examples/assets/public/guide.txt +1 -0
  183. package/examples/assets/tests/requests.json +44 -0
  184. package/examples/assets/urlcode.yaml +27 -0
  185. package/examples/aws/README.md +13 -0
  186. package/examples/aws/handler.mjs +5 -0
  187. package/examples/aws/public/index.html +3 -0
  188. package/examples/aws/public/notes.txt +1 -0
  189. package/examples/aws/template.yaml +33 -0
  190. package/examples/aws/tests/requests.json +9 -0
  191. package/examples/aws/urlcode.yaml +19 -0
  192. package/examples/cloudflare/README.md +18 -0
  193. package/examples/cloudflare/package.json +14 -0
  194. package/examples/cloudflare/tests/requests.json +9 -0
  195. package/examples/cloudflare/urlcode.yaml +24 -0
  196. package/examples/cloudflare/wrangler.toml +5 -0
  197. package/examples/compliance/README.md +20 -0
  198. package/examples/compliance/rules.mjs +65 -0
  199. package/examples/cookbook/README.md +21 -0
  200. package/examples/cookbook/functions/choice.mjs +4 -0
  201. package/examples/cookbook/functions/echo.mjs +3 -0
  202. package/examples/cookbook/functions/hello.mjs +3 -0
  203. package/examples/cookbook/functions/text.mjs +3 -0
  204. package/examples/cookbook/middleware/headers.mjs +6 -0
  205. package/examples/cookbook/public/about.html +1 -0
  206. package/examples/cookbook/public/assets/index.html +1 -0
  207. package/examples/cookbook/public/assets/site.css +1 -0
  208. package/examples/cookbook/public/favicon.svg +1 -0
  209. package/examples/cookbook/public/guide.txt +1 -0
  210. package/examples/cookbook/public/llms.txt +6 -0
  211. package/examples/cookbook/routes/code.yaml +55 -0
  212. package/examples/cookbook/routes/files.yaml +17 -0
  213. package/examples/cookbook/routes/policies.yaml +28 -0
  214. package/examples/cookbook/routes/redirects.yaml +38 -0
  215. package/examples/cookbook/routes/responses.yaml +26 -0
  216. package/examples/cookbook/tests/requests.json +254 -0
  217. package/examples/cookbook/urlcode.yaml +22 -0
  218. package/examples/live-links/README.md +11 -0
  219. package/examples/live-links/tests/requests.json +6 -0
  220. package/examples/live-links/urlcode.yaml +16 -0
  221. package/examples/monitoring/blackbox-jobs.yaml +37 -0
  222. package/examples/monitoring/prometheus-rules.yaml +88 -0
  223. package/examples/monitoring/prometheus-scrape.yaml +20 -0
  224. package/examples/monitoring/vector.toml +65 -0
  225. package/examples/prerender/README.md +48 -0
  226. package/examples/prerender/functions/page.mjs +6 -0
  227. package/examples/prerender/middleware/template.mjs +29 -0
  228. package/examples/prerender/prerender.d.mts +4 -0
  229. package/examples/prerender/prerender.mjs +63 -0
  230. package/examples/prerender/tests/requests.json +39 -0
  231. package/examples/prerender/urlcode.yaml +50 -0
  232. package/examples/tunnel/dev-with-ngrok.sh +61 -0
  233. package/examples/vercel/README.md +13 -0
  234. package/examples/vercel/api/index.js +5 -0
  235. package/examples/vercel/package.json +8 -0
  236. package/examples/vercel/public/index.html +3 -0
  237. package/examples/vercel/public/notes.txt +1 -0
  238. package/examples/vercel/tests/requests.json +48 -0
  239. package/examples/vercel/urlcode.yaml +19 -0
  240. package/examples/vercel/vercel.json +10 -0
  241. package/llms.txt +46 -0
  242. package/package.json +123 -0
  243. package/schemas/urlcode.schema.json +1230 -0
  244. package/starters/default/.gitattributes +1 -0
  245. package/starters/default/Makefile +30 -0
  246. package/starters/default/README.md +34 -0
  247. package/starters/default/functions/hello.mjs +3 -0
  248. package/starters/default/gitignore.template +8 -0
  249. package/starters/default/middleware/headers.mjs +6 -0
  250. package/starters/default/routes/functions.yaml +20 -0
  251. package/starters/default/routes/marketing/links.yaml +7 -0
  252. package/starters/default/starter.json +5 -0
  253. package/starters/default/tests/requests.json +56 -0
  254. package/starters/default/urlcode.yaml +8 -0
package/docs/VERCEL.md ADDED
@@ -0,0 +1,107 @@
1
+ # Deploying to Vercel
2
+
3
+ The Vercel adapter runs a URLCode project as a Node function. The same
4
+ `urlcode.yaml` that runs locally or in a container serves the deployment —
5
+ that is the point of the project format.
6
+
7
+ **This adapter serves native handlers only:** redirects, validated responses,
8
+ pages, static assets and downloads. Isolated functions, middleware and stored
9
+ live links are refused at activation, not per request, so a deployment cannot
10
+ half-work. See [what is not supported](#what-this-adapter-does-not-do).
11
+
12
+ A working project is in [`examples/vercel/`](../examples/vercel/).
13
+
14
+ ## Set it up
15
+
16
+ ```js
17
+ // api/index.js
18
+ import { createVercelHandler } from '@jimhoyd/urlcode/vercel';
19
+
20
+ export default createVercelHandler({ project: process.cwd() });
21
+ ```
22
+
23
+ ```json
24
+ {
25
+ "functions": {
26
+ "api/index.js": {
27
+ "runtime": "nodejs22.x",
28
+ "includeFiles": "{urlcode.yaml,routes/**,public/**}"
29
+ }
30
+ },
31
+ "rewrites": [{ "source": "/(.*)", "destination": "/api" }]
32
+ }
33
+ ```
34
+
35
+ Two details decide whether this works:
36
+
37
+ - **`includeFiles`** must cover every file the project reads: the entry YAML,
38
+ any `includes`, and every page, download and static directory. They are read
39
+ at activation, so a missing one fails the whole deployment rather than one
40
+ route. Node 22.13 or newer is required.
41
+ - **The rewrite** sends every path to the handler, because URLCode owns routing.
42
+ Anything you leave outside it is served by Vercel, not by your project.
43
+
44
+ ## Bindings
45
+
46
+ A self-hosted deployment grants `env` and `secrets` through an operator policy
47
+ file outside the project. Vercel has no such place, so the adapter reads the
48
+ same document from the **`URLCODE_POLICY`** environment variable:
49
+
50
+ ```sh
51
+ urlcode permissions --project . # prints the grant document
52
+ vercel env add URLCODE_POLICY # paste it
53
+ ```
54
+
55
+ It is validated exactly as the file is, including the `projectSha256` pin — so a
56
+ policy issued for one revision does not activate another. Change a route or a
57
+ binding and the grant must be reissued, which is the intended friction. The
58
+ values themselves are ordinary Vercel environment variables; the policy decides
59
+ which routes may read them.
60
+
61
+ ## Origin
62
+
63
+ Functions and absolute URLs see the origin the adapter resolves, in order:
64
+ the `origin` option, `URLCODE_ORIGIN`, then Vercel's own
65
+ `VERCEL_PROJECT_PRODUCTION_URL`, `VERCEL_URL` or `VERCEL_BRANCH_URL`. Those are
66
+ platform-set, not client-supplied — forwarded headers stay untrusted here as
67
+ everywhere else. Set `URLCODE_ORIGIN` explicitly when you serve a custom domain
68
+ and want it in generated URLs.
69
+
70
+ ## What this adapter does not do
71
+
72
+ | Not supported | Why |
73
+ |---|---|
74
+ | Isolated functions | Every cold start would spawn worker threads and load the WASM engine. Correctness is not the issue; predictable latency is, and it is unmeasured. |
75
+ | Middleware | Runs in the same sandbox as functions. |
76
+ | Stored live links | SQLite needs a durable writable file. A serverless filesystem is ephemeral and per-instance, so records would silently diverge between instances. |
77
+ | `urlcode serve` operational endpoints | `/_urlcode/health` and `/_urlcode/ready` describe a long-lived process. Use Vercel's own observability. |
78
+
79
+ Each refusal happens at activation with a message naming the route, so you find
80
+ out on deploy rather than on a request.
81
+
82
+ ## Operating it
83
+
84
+ Every instance activates the project independently: parsing YAML, snapshotting
85
+ assets and compiling routes on each cold start. Keep asset snapshots small — the
86
+ [capacity limits](CAPACITY.md) apply per instance, and a 64 MiB snapshot is 64
87
+ MiB in every concurrent instance.
88
+
89
+ There is no reload: a deployment serves the revision it was built from, which is
90
+ what you want from immutable deployments. Ship a change by deploying.
91
+
92
+ Logs go to Vercel's collector rather than to a stdout stream you control, so the
93
+ [monitoring recipes](MONITORING.md) that parse JSON records need adapting;
94
+ the record fields are the same.
95
+
96
+ ## Verification status
97
+
98
+ The adapter is tested against the self-hosted runtime for byte-identical status,
99
+ body and headers across redirects, parameters, responses, pages, static files
100
+ and misses, and for refusing unsupported handlers, enforcing the policy pin and
101
+ bounding request bodies.
102
+
103
+ **It has not been deployed to Vercel.** Those tests drive the real handler over
104
+ a real Node request, which is the shape Vercel invokes, but no run on the
105
+ platform has happened. Treat `includeFiles` coverage, cold-start latency and
106
+ custom-domain behaviour as unverified until you deploy the example and see it
107
+ work.
@@ -0,0 +1,521 @@
1
+ # YAML guide and recipe book
2
+
3
+ This guide targets URLCode 0.2.0. Start with the function example below,
4
+ then add only the fields your route needs. The authoritative machine-readable
5
+ shape is [JSON Schema](../schemas/urlcode.schema.json); semantic rules are in the
6
+ [specification](SPECIFICATION.md). Unsupported fields fail validation.
7
+
8
+ ## Run all the examples
9
+
10
+ The [cookbook project](../examples/cookbook/urlcode.yaml) includes the six stateless handler
11
+ types, middleware, typed/defaulted inputs, methods, response headers, body checks,
12
+ expiry and file organization. Its referenced JavaScript and assets are included.
13
+ From the runtime checkout:
14
+
15
+ ```sh
16
+ npm ci
17
+ node src/cli.ts validate --project examples/cookbook
18
+ node src/cli.ts test --project examples/cookbook
19
+ node src/cli.ts audit --project examples/cookbook --expect-routes 25
20
+ node src/cli.ts dev --project examples/cookbook
21
+ ```
22
+
23
+ The cookbook is a larger learning project. The normal `urlcode init ../my-links`
24
+ remains a small two-route starter. For an independent application with a pinned
25
+ runtime dependency, clone [urlcode-template](https://github.com/jimhoyd-com/urlcode-template).
26
+
27
+ ## 1. A URL that runs code
28
+
29
+ A complete `urlcode.yaml`:
30
+
31
+ ```yaml
32
+ version: "1"
33
+ routes:
34
+ /hello/{name}:
35
+ parameters:
36
+ - name: name
37
+ in: path
38
+ required: true
39
+ schema: {type: string, minLength: 1, maxLength: 80}
40
+ function:
41
+ source: functions/hello.mjs
42
+ args:
43
+ name: {from: path, name: name}
44
+ env:
45
+ GREETING: {value: Hello}
46
+ ```
47
+
48
+ Create `functions/hello.mjs`:
49
+
50
+ ```js
51
+ export default function hello(request, {args, env}) {
52
+ return Response.json({message: `${env.GREETING}, ${args.name}!`});
53
+ }
54
+ ```
55
+
56
+ GET `/hello/Ada` returns JSON. HEAD invokes the function and suppresses the body.
57
+ Methods default to GET and HEAD. Function paths resolve from the project root,
58
+ not the YAML file's directory. `.js` and `.mjs` ES modules work; TypeScript, Node
59
+ APIs, npm imports, network access and filesystem access do not.
60
+
61
+ ## 2. Ordinary and permanent redirects
62
+
63
+ The following snippets are **entries under `routes:`** unless stated otherwise:
64
+
65
+ ```yaml
66
+ /go:
67
+ redirect:
68
+ url: https://example.com/
69
+ /moved:
70
+ redirect:
71
+ url: https://example.com/new
72
+ status: 308
73
+ response:
74
+ headers:
75
+ Cache-Control: public, max-age=60
76
+ ```
77
+
78
+ 302 is the default. Allowed codes are 301, 302, 303, 307 and 308. Choose status
79
+ and cache policy deliberately: a cached permanent redirect can outlive a server
80
+ rollback. Requests' query strings are not forwarded by default. No function or
81
+ middleware means no sandbox execution for these routes.
82
+
83
+ ## 3. Parameterized redirects and explicit query forwarding
84
+
85
+ ```yaml
86
+ /product/{id}:
87
+ parameters:
88
+ - name: id
89
+ in: path
90
+ required: true
91
+ schema: {type: string, minLength: 1, maxLength: 64}
92
+ - name: page
93
+ in: query
94
+ schema: {type: integer, minimum: 1, maximum: 100, default: 1}
95
+ - name: tag
96
+ in: query
97
+ schema: {type: array, items: {type: string}, maxItems: 3}
98
+ - name: x-channel
99
+ in: header
100
+ schema: {type: string, enum: [web, email], default: web}
101
+ redirect:
102
+ url: https://example.com/products/{id}
103
+ query:
104
+ map:
105
+ page: {from: query, name: page}
106
+ label: {from: query, name: tag}
107
+ channel: {from: header, name: x-channel}
108
+ pass: [utm_source]
109
+ ```
110
+
111
+ `/product/abc?page=2&tag=red&tag=blue&utm_source=news&ignored=no` redirects to
112
+ `https://example.com/products/abc?page=2&label=red&label=blue&channel=web&utm_source=news`.
113
+ The unknown `ignored` key is dropped. Missing `page` becomes 1. Arrays produce
114
+ repeated destination keys. Invalid page values return 400 before redirecting.
115
+
116
+ Destination hosts/schemes are literal HTTP(S); path placeholders are safely
117
+ encoded. No arbitrary input-controlled host, credentials, secret interpolation,
118
+ or unrestricted `pass: true`. Mapping/passthrough keys must not collide with
119
+ existing destination keys. Header inputs are client-supplied values, not proof
120
+ of identity. See [input and redirect semantics](SPECIFICATION.md).
121
+
122
+ ## 4. Input types and constraints
123
+
124
+ Use this list under a route's `parameters` when those inputs are needed:
125
+
126
+ ```yaml
127
+ parameters:
128
+ - name: search
129
+ in: query
130
+ required: true
131
+ schema: {type: string, minLength: 1, maxLength: 200}
132
+ - name: page
133
+ in: query
134
+ schema: {type: integer, minimum: 1, default: 1}
135
+ - name: weight
136
+ in: query
137
+ schema: {type: number, minimum: 0, maximum: 1}
138
+ - name: preview
139
+ in: query
140
+ schema: {type: boolean, default: false}
141
+ - name: category
142
+ in: query
143
+ schema: {type: string, enum: [docs, news], default: docs}
144
+ - name: ids
145
+ in: query
146
+ schema: {type: array, items: {type: integer}, maxItems: 10}
147
+ ```
148
+
149
+ Path inputs must be required strings with no default. Query/header scalar types
150
+ are string, integer, number and boolean; arrays are query-only. Booleans are
151
+ exactly `true`/`false`; numbers do not accept exponent notation or whitespace.
152
+ Defaults apply to absence, not empty strings. Duplicate scalar values fail.
153
+ Required, missing and invalid inputs return 400. This is a documented subset,
154
+ not full OpenAPI/JSON Schema: no `pattern`, `format`, nested input objects,
155
+ `oneOf`, `style` or `explode` in parameter schemas.
156
+
157
+ ## 5. Methods and body validation
158
+
159
+ ```yaml
160
+ /echo:
161
+ methods: [POST]
162
+ request:
163
+ body:
164
+ required: true
165
+ maxBytes: 4096
166
+ contentTypes: [application/json]
167
+ format: json
168
+ function:
169
+ source: functions/echo.mjs
170
+ ```
171
+
172
+ ```js
173
+ export default async function echo(request) {
174
+ return Response.json(await request.json());
175
+ }
176
+ ```
177
+
178
+ This validates JSON syntax/media type/UTF-8 and body size, not an application
179
+ object schema. Validate business fields in code. Empty required body: 400;
180
+ oversized body: 413; wrong media type: 415. For text, use `contentTypes:
181
+ [text/plain]`, `format: text`, and `request.text()`; see the runnable `/text`
182
+ recipe. `maxBytes: 0` can reject nonempty bodies. Bodies are buffered, not streamed.
183
+
184
+ Allowed methods: GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS. An explicit list
185
+ replaces the defaults: `[GET]` does not add HEAD. Wrong method returns 405 with
186
+ Allow. Asset handlers accept only GET/HEAD. Body bytes are not automatically
187
+ mapped to function arguments.
188
+
189
+ ## 6. All function argument sources
190
+
191
+ Within `function.args`, supported values are:
192
+
193
+ ```yaml
194
+ args:
195
+ label: campaign
196
+ attempts: 3
197
+ preview: false
198
+ code: {from: path, name: code}
199
+ page: {from: query, name: page}
200
+ channel: {from: header, name: x-channel}
201
+ greeting: {env: GREETING}
202
+ token: {secret: TOKEN}
203
+ ```
204
+
205
+ This is a field-shape illustration: declare the referenced path/query/header
206
+ inputs and route binding aliases before using it. Null, array and arbitrary
207
+ object arguments are not supported. Read `context.args` or directly access
208
+ `context.inputs.path/query/header`, `context.env` and `context.secrets`.
209
+ `function.export` selects a named export; omit it for `default`.
210
+
211
+ For a dynamic redirect, use validated choices instead of accepting any URL:
212
+
213
+ ```js
214
+ export default function choice(request, {args}) {
215
+ const destinations = {docs: 'https://example.com/docs', home: 'https://example.com/'};
216
+ return Response.redirect(destinations[args.destination], 302);
217
+ }
218
+ ```
219
+
220
+ The runnable `/choice` recipe declares an enum query input and binds it to args.
221
+ Functions can return `Response.json(...)`, `new Response('text', {status, headers})`,
222
+ or `Response.redirect(...)`. HTML is a string response with Content-Type text/html;
223
+ escape untrusted values yourself. See the exact [guest API](SPECIFICATION.md#functions).
224
+
225
+ ## 7. Middleware before and after a handler
226
+
227
+ ```yaml
228
+ /go-with-header:
229
+ middleware:
230
+ - source: middleware/headers.mjs
231
+ export: decorate
232
+ redirect: {url: 'https://example.com/'}
233
+ ```
234
+
235
+ ```js
236
+ export async function decorate(request, context, next) {
237
+ context.state.example = 'cookbook';
238
+ const response = await next();
239
+ response.headers.set('x-middleware', context.state.example);
240
+ return response;
241
+ }
242
+ ```
243
+
244
+ Entries run in declared order before the handler and reverse order afterward.
245
+ Return a Response early to skip downstream code; call `next()` at most once.
246
+ Up to 16 middleware entries share one sandbox/deadline and approved route
247
+ bindings. Native bodies are opaque; preserving them requires retaining original
248
+ status/headers. Return a new Response to replace native content or destination.
249
+ YAML response headers apply last. See [middleware](MIDDLEWARE.md) for details.
250
+
251
+ ## 8. Native responses, headers and cookies
252
+
253
+ ```yaml
254
+ /status:
255
+ respond:
256
+ json: {ok: true, service: my-links}
257
+ /notice:
258
+ respond:
259
+ status: 503
260
+ text: Temporarily unavailable
261
+ response:
262
+ headers:
263
+ Retry-After: "60"
264
+ /empty:
265
+ respond: {status: 204}
266
+ /cookie-demo:
267
+ respond: {text: Non-secret preferences only}
268
+ response:
269
+ headers:
270
+ Set-Cookie:
271
+ - "theme=light; Path=/; Secure; SameSite=Lax"
272
+ - "notice=seen; Path=/; HttpOnly; Secure; SameSite=Lax"
273
+ ```
274
+
275
+ `respond` defaults to 200. Use text or JSON, never both. Omit both for an empty
276
+ body; 204/205 must have no body. 206/304 belong to native asset handling.
277
+ Header values are literal strings; quote numeric-looking values. Only Set-Cookie
278
+ accepts arrays. Do not put live session tokens in YAML. Secure cookies require
279
+ HTTPS at the browser. Functions can create dynamic cookies, but no cookie
280
+ parsing/signing/authentication framework is built in.
281
+
282
+ Do not set Content-Length, Location, Allow, ETag, Content-Range or other
283
+ runtime-owned headers in YAML. Use the corresponding handler. The full reserved
284
+ list and precedence rules are in [HTTP](HTTP.md).
285
+
286
+ ## 9. Explicit OPTIONS response (not automatic CORS)
287
+
288
+ ```yaml
289
+ /preflight:
290
+ methods: [OPTIONS]
291
+ respond: {status: 204}
292
+ response:
293
+ headers:
294
+ Access-Control-Allow-Origin: https://app.example.com
295
+ Access-Control-Allow-Methods: GET, HEAD
296
+ Access-Control-Allow-Headers: Content-Type
297
+ ```
298
+
299
+ This teaches declared headers only; it is not a working cross-origin GET API.
300
+ For a real API, OPTIONS and the actual methods must be handled on the same URL,
301
+ and actual responses also need the appropriate CORS headers. Because one path
302
+ has one handler, use a function with `[GET, HEAD, OPTIONS]` to branch on method.
303
+ Never reflect arbitrary Origin with credentials. Automatic CORS is unsupported.
304
+
305
+ ## 10. Pages, static folders, downloads and MIME
306
+
307
+ ```yaml
308
+ /about:
309
+ page:
310
+ file: public/about.html
311
+ cacheControl: no-cache
312
+ /assets/*:
313
+ static:
314
+ directory: public/assets
315
+ index: index.html
316
+ cacheControl: public, max-age=3600
317
+ /download:
318
+ download:
319
+ file: public/guide.txt
320
+ filename: urlcode-guide.txt
321
+ contentType: text/plain
322
+ cacheControl: no-store
323
+ ```
324
+
325
+ All files must exist. MIME is detected by extension, not content sniffing; unknown
326
+ extensions become application/octet-stream. `contentType` overrides detection
327
+ without MIME parameters. An override on a static mount affects all its files.
328
+ The download name defaults to the source basename. `index` is opt-in and only
329
+ applies to slash-terminated requests. No automatic slash redirect or SPA fallback.
330
+
331
+ Cache choices: `no-cache` (asset default), `no-store`, `public, max-age=3600`,
332
+ `public, max-age=31536000, immutable`. Reserve immutable caching for versioned
333
+ URLs. GET/HEAD, ETag/date validation and single byte ranges are supported.
334
+ Files stay snapshotted until reload/restart. See [assets](ASSETS.md) for complete
335
+ conditional/range semantics and publication safety. Files are limited to 16 MiB
336
+ each and 64 MiB total unique bytes per snapshot.
337
+
338
+ ## 11. Enable, disable and expire
339
+
340
+ ```yaml
341
+ /paused:
342
+ enabled: false
343
+ redirect: {url: 'https://example.com/'}
344
+ /campaign:
345
+ description: A scheduled end, no scheduled start
346
+ expires: "2030-01-01T00:00:00Z"
347
+ redirect: {url: 'https://example.com/'}
348
+ ```
349
+
350
+ Quote timestamps so they remain strings. Disabled routes return 404; expired
351
+ routes return 410. Expiry is an absolute UTC timestamp, not a TTL. There is no
352
+ start-time scheduler. `description` is authoring metadata. Changing YAML activates
353
+ through dev reload or production restart/deployment; it is not an HTTP mutation.
354
+
355
+ ## 12. Environment and secret references
356
+
357
+ Route-level shape (references only, never secret values):
358
+
359
+ ```yaml
360
+ env:
361
+ GREETING: {value: Hello}
362
+ REGION: {env: APP_REGION}
363
+ secrets:
364
+ TOKEN: {secret: APP_TOKEN}
365
+ ```
366
+
367
+ Literal non-secret env needs no grant. External env and secrets require an
368
+ operator-owned policy outside the checkout, granting exact names to the route
369
+ and pinning the reviewed config/code digest. `urlcode permissions --project
370
+ ./my-links` prints a proposed policy; review it and store it outside the app.
371
+ Then pass `--policy /operator/path/policy.json` to validate/dev/test/serve.
372
+ This inspection does not authorize the project or execute its code.
373
+
374
+ Use ignored `.env.local` for local values; process environment wins. Production
375
+ `serve` reads process environment, never `.env.local`. Let your supervisor resolve
376
+ provider secrets and inject them; direct provider secret-store adapters do not
377
+ exist yet. Every config/code change invalidates the grant; rotate values by
378
+ restarting/redeploying. Never return a secret in an example response. Middleware
379
+ and functions on an approved route can read its bindings. See [policy setup](FUNCTION-SECURITY.md).
380
+
381
+ ## 13. Split files and folders
382
+
383
+ Complete entry point:
384
+
385
+ ```yaml
386
+ version: "1"
387
+ includes:
388
+ - routes/code.yaml
389
+ - routes/marketing/links.yaml
390
+ routes: {}
391
+ ```
392
+
393
+ Each included file contains `version: "1"` and `routes`. No nested includes,
394
+ globs, anchors, merge keys or remote includes. References always use project-root
395
+ paths. Duplicate routes fail; include order is not priority. See [organization](ORGANIZATION.md)
396
+ and [matching/regex/wildcard rules](ROUTING.md).
397
+
398
+ ## 14. Assert inputs and outputs
399
+
400
+ Save a JSON array as `tests/requests.json`:
401
+
402
+ ```json
403
+ [
404
+ {"path":"/hello/Ada","status":200,"expectBody":"{\"message\":\"Hello, Ada!\"}"},
405
+ {"path":"/hello/Ada","method":"HEAD","status":200,"expectBody":""},
406
+ {"path":"/go","status":302,"expectHeaders":{"location":"https://example.com/"}},
407
+ {"path":"/go","method":"POST","status":405,"expectHeaders":{"allow":"GET, HEAD"}}
408
+ ]
409
+ ```
410
+
411
+ This fixture targets recipes 1 and 2 together; the runnable cookbook has its own
412
+ matching expectations. Supported test fields: `path`, optional `method`, request
413
+ `headers` and string `body`, required `status`, optional exact string `expectBody`
414
+ and string-map `expectHeaders`. Tests do not follow external redirects. There
415
+ are no route-local YAML test fields or JSON-path assertions yet.
416
+
417
+ Run validate, test, routes, and audit with an intentional expected count. Test
418
+ both positive and negative inputs, every allowed method, HEAD, middleware short
419
+ circuits and relevant asset conditions. Audit needs meaningful body/header
420
+ assertions; a status-only success is insufficient. Benchmarks and recovery drills
421
+ are separate from functional correctness. See [readiness](READINESS.md).
422
+
423
+ See [organization and readability practices](BEST-PRACTICES.md) for conventions
424
+ that keep larger projects easy to maintain.
425
+
426
+ ## 15. Live short-link records
427
+
428
+ Set `dynamicLinks: true` in the entry `urlcode.yaml` before adding this route.
429
+ It defaults to false and cannot be enabled by an included file.
430
+
431
+ ```yaml
432
+ /r/{code}:
433
+ parameters:
434
+ - name: code
435
+ in: path
436
+ required: true
437
+ schema: {type: string, minLength: 1, maxLength: 128}
438
+ link:
439
+ collection: links
440
+ code: {from: path, name: code}
441
+ ```
442
+
443
+ The seventh handler resolves stored records without rebuilding YAML. It requires
444
+ an external operator store binding; the [live-link example](../examples/live-links/README.md)
445
+ has separate setup and fixtures. See [dynamic links](DYNAMIC-LINKS.md) for CLI/API
446
+ creation, optimistic updates, disabled/expired records, persistence and backups.
447
+ This is not a general database capability for sandboxed functions.
448
+
449
+ ## 16. Hardened profile and per-route overrides
450
+
451
+ ```yaml
452
+ version: "1"
453
+ policies:
454
+ profile: hardened # security, agents, throttle, compression, cache
455
+ throttle: { quota: 60, window: 60 } # tighten one number; the rest stays
456
+ profiles:
457
+ public-api:
458
+ security: { headers: oshp-no-csp, set: { x-robots-tag: noindex } }
459
+ cache: { strategy: swr, maxAge: 30, staleWhileRevalidate: 300 }
460
+ routes:
461
+ /:
462
+ page: {source: pages/index.html}
463
+ /api/lookup/{id}:
464
+ parameters:
465
+ - {name: id, in: path, required: true, schema: {type: string, maxLength: 64}}
466
+ function: {source: functions/lookup.mjs}
467
+ policies:
468
+ profile: public-api # merges over the project layer
469
+ throttle: {quota: 10, window: 60, partition: client-route}
470
+ /healthz:
471
+ respond: {text: ok}
472
+ policies: {throttle: false, agents: false}
473
+ ```
474
+
475
+ Everything under `policies` is optional and off unless declared. The project
476
+ block sets defaults, a route block adjusts them, `false` removes one policy for
477
+ that route and an object merges shallowly over what is below it. `hardened`
478
+ is the only built-in profile; `profiles` defines your own. Serve with
479
+ `--trusted-proxies` when a proxy sits in front so `client` partitioning sees
480
+ the real peer. Not every target accepts every policy: see the per-target
481
+ table in [policies](POLICIES.md) before deploying the same YAML to an adapter.
482
+
483
+ ## 17. Site conventions
484
+
485
+ Generate the well-known files from a top-level `site` block instead of writing
486
+ them by hand. Each declared key becomes one native route counted by
487
+ `routes`/`audit`; a declared route at the same path wins.
488
+
489
+ ```yaml
490
+ site:
491
+ robots:
492
+ disallow: [ai-crawlers, /admin]
493
+ sitemap: true # Sitemap line needs --origin
494
+ favicon: public/favicon.svg # served at /favicon.ico
495
+ securityTxt:
496
+ contact: [mailto:security@example.com]
497
+ expires: "2099-01-01T00:00:00Z"
498
+ llms: public/llms.txt # served at /llms.txt
499
+ # sitemap: true # /sitemap.xml; refuses to start without --origin
500
+ ```
501
+
502
+ See [site conventions](SITE.md) for every field, exclusions and target support.
503
+
504
+ ## Common mistakes
505
+
506
+ | Mistake | Correction |
507
+ |---|---|
508
+ | Two handlers on one route | Choose exactly one; put reusable logic in middleware |
509
+ | `/r/:id`, `/r/{id:.*}` or a regex | Use `/r/{id}` plus a required path input; no regex/greedy matching |
510
+ | `${TOKEN}` or `process.env` | Use declared binding references and an external operator grant |
511
+ | `fetch`, npm or Node imports | Unsupported in the guest; do not claim a network/storage integration |
512
+ | Asset MIME/header overrides in `response.headers` | Configure `contentType`, `cacheControl`, `filename` on the asset handler |
513
+ | `methods: [GET]` expecting HEAD | Declare HEAD too or omit methods for default GET/HEAD |
514
+ | YAML fields for rate limits/workers/DNS/TLS | Deployment controls live outside portable route YAML |
515
+ | YAML aliases, anchors or implicit date objects | Use plain JSON-compatible YAML and quoted timestamps |
516
+ | Automatic hot updates in `serve` | Deploy/restart or use the embedding reload API deliberately |
517
+ | “All examples are production-ready” | Validate your security, load and deployment requirements separately |
518
+
519
+ Live-link recipes require `dynamicLinks: true` in the entry `urlcode.yaml`. It is
520
+ false by default and cannot be set in included route files. Parameterized routes
521
+ and functions alone do not need it. See [dynamic-link opt-in](DYNAMIC-LINKS.md#explicit-project-opt-in).