@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
@@ -0,0 +1,15 @@
1
+ [
2
+ {"path":"/todos","method":"POST","headers":{"content-type":"application/json"},"body":"{\"title\":\"Buy milk\"}","status":201,"expectBody":"{\"ok\":true}"},
3
+ {"path":"/todos","method":"POST","headers":{"content-type":"application/json"},"body":"{\"title\":5}","status":422},
4
+ {"path":"/todos","method":"POST","headers":{"content-type":"application/json"},"body":"{\"title\":\"x\",\"extra\":1}","status":422},
5
+ {"path":"/todos","method":"POST","headers":{"content-type":"application/json","accept":"application/json"},"body":"{\"title\":5}","status":422,"expectHeaders":{"content-type":"application/json"},"expectBody":"{\"error\":\"body_validation_failed\",\"message\":\"Request body failed validation\",\"issues\":[{\"pointer\":\"/title\",\"keyword\":\"type\",\"message\":\"must be a string\",\"expected\":\"string\"}]}"},
6
+ {"path":"/todos","method":"POST","headers":{"content-type":"application/json","accept":"*/*"},"body":"{\"title\":5}","status":422,"expectHeaders":{"content-type":"text/plain; charset=utf-8"},"expectBody":"Request body failed validation\n/title must be a string\n"},
7
+ {"path":"/todos","method":"POST","headers":{"content-type":"application/json"},"body":"{nope","status":400},
8
+ {"path":"/todos","method":"POST","headers":{"content-type":"text/plain"},"body":"hi","status":415},
9
+ {"path":"/todos/123e4567-e89b-42d3-a456-426614174000","status":200,"expectBody":"{\"ok\":true}"},
10
+ {"path":"/todos/123e4567-e89b-42d3-a456-426614174000","method":"HEAD","status":200,"expectBody":""},
11
+ {"path":"/todos/not-a-uuid","status":400},
12
+ {"path":"/tags?slug=ok-1","status":200,"expectBody":"{\"ok\":true}"},
13
+ {"path":"/tags?slug=ok-1","method":"HEAD","status":200,"expectBody":""},
14
+ {"path":"/tags?slug=Bad_Slug","status":400}
15
+ ]
@@ -0,0 +1,34 @@
1
+ version: "1"
2
+ routes:
3
+ /todos:
4
+ methods: [POST]
5
+ request:
6
+ body:
7
+ maxBytes: 4096
8
+ contentTypes: [application/json]
9
+ format: json
10
+ required: true
11
+ schema:
12
+ type: object
13
+ required: [title]
14
+ additionalProperties: false
15
+ properties:
16
+ title: {type: string, minLength: 1, maxLength: 200}
17
+ description: {type: string, maxLength: 2000}
18
+ completed: {type: boolean}
19
+ respond:
20
+ status: 201
21
+ json: {ok: true}
22
+ /todos/{id}:
23
+ parameters:
24
+ - {name: id, in: path, required: true, schema: {type: string, format: uuid}}
25
+ respond:
26
+ json: {ok: true}
27
+ /tags:
28
+ parameters:
29
+ - name: slug
30
+ in: query
31
+ required: true
32
+ schema: {type: string, pattern: "^[a-z0-9-]+$", maxLength: 32}
33
+ respond:
34
+ json: {ok: true}
@@ -0,0 +1,8 @@
1
+ # Audit coverage waiver
2
+
3
+ `urlcode audit --project examples/coverage-waiver` reports `ready: true` although
4
+ `POST /notes` has no fixture, because the route declares `coveredElsewhere` for
5
+ that method with a reason. The pair is still listed under `waivedRouteMethods`.
6
+ A waiver is honored only when the route has another passing normal-response
7
+ fixture (here `GET`). See
8
+ [readiness](../../docs/READINESS.md#waive-a-method-covered-elsewhere).
@@ -0,0 +1,16 @@
1
+ id: coverage-waiver
2
+ description: A stateful POST route waived from audit coverage with a required reason, kept visible under waivedRouteMethods.
3
+ tags: [audit, readiness, coverage, waiver, covered-elsewhere,function, post]
4
+ complexity: intermediate
5
+ capabilities: [enabled, function, methods]
6
+ targets: {self-hosted: compatible, aws: refused, vercel: refused, cloudflare: refused, static: refused}
7
+ routes: 1
8
+ files: [urlcode.yaml, functions/notes.mjs, tests/requests.json, README.md]
9
+ tests:
10
+ fixtures: tests/requests.json
11
+ commands:
12
+ - node src/cli.ts validate --local --project examples/coverage-waiver
13
+ - node src/cli.ts test --project examples/coverage-waiver
14
+ - node src/cli.ts audit --project examples/coverage-waiver --expect-routes 1
15
+ behavior:
16
+ - POST /notes is waived from coverage with a reason; GET is asserted by a fixture; the waiver is listed by audit, not hidden
@@ -0,0 +1,2 @@
1
+ export default (request) =>
2
+ request.method === 'POST' ? new Response('created', { status: 201 }) : new Response('notes');
@@ -0,0 +1,3 @@
1
+ [
2
+ { "path": "/notes", "status": 200, "expectBody": "notes" }
3
+ ]
@@ -0,0 +1,10 @@
1
+ version: "1"
2
+ routes:
3
+ /notes:
4
+ methods: [GET, POST]
5
+ # POST creates a note in a real store, so a fixture would mutate it. The
6
+ # reason is required and is listed by `urlcode audit` under waivedRouteMethods.
7
+ coveredElsewhere:
8
+ POST: "Creates a note; covered by the store's integration tests, not by a repeatable fixture."
9
+ function:
10
+ source: functions/notes.mjs
@@ -0,0 +1,39 @@
1
+ # Data directory example
2
+
3
+ A trusted function that reads files from a directory the operator picks. The
4
+ project declares only the *name* `DATA_DIR`; the host supplies the value, so a
5
+ test run, a benchmark or a deployment can point the same reviewed project at a
6
+ different directory without copying it.
7
+
8
+ ```yaml
9
+ routes:
10
+ /notes/{name}:
11
+ function: functions/note.mjs
12
+ env:
13
+ DATA_DIR: {env: DATA_DIR}
14
+ ```
15
+
16
+ The function receives the resolved value as `env.DATA_DIR` in its second
17
+ argument. It does not read `process.env`, so the binding stays visible to
18
+ `permissions` and `audit`. Validate the request-supplied file name before
19
+ joining it to the directory; the function does that with an allowlist pattern.
20
+
21
+ There is no default: a binding the operator did not grant, or a `DATA_DIR` that
22
+ is not set, refuses to activate rather than falling back to a directory the
23
+ project chose. Use `{value: ...}` only for a fixed literal.
24
+
25
+ Run it from the runtime checkout. The policy lives outside the project, and the
26
+ `--policy` file must be regenerated whenever the config or function changes:
27
+
28
+ ```sh
29
+ urlcode permissions --project examples/data-dir > /path/outside/project/policy.json
30
+ export DATA_DIR=./examples/data-dir/data # relative paths resolve from the process working directory
31
+ urlcode validate --local --project examples/data-dir --policy /path/outside/project/policy.json
32
+ urlcode test --project examples/data-dir --policy /path/outside/project/policy.json
33
+ urlcode audit --project examples/data-dir --expect-routes 1 --policy /path/outside/project/policy.json
34
+ ```
35
+
36
+ For local development, `DATA_DIR=...` may live in an ignored `.env.local`;
37
+ production `serve` reads only the process environment. See
38
+ [bindings](../../docs/yaml/organization.md#12-environment-and-secret-references)
39
+ and [policy setup](../../docs/FUNCTION-SECURITY.md).
@@ -0,0 +1 @@
1
+ hello from the data directory
@@ -0,0 +1,22 @@
1
+ id: data-dir
2
+ description: A trusted function reads files from a data directory the operator chooses through an {env} binding, so a test or deployment can point the same project at another directory.
3
+ tags: [env, binding, data, directory, file, function, trusted, policy, operator, data-dir]
4
+ complexity: intermediate
5
+ capabilities: [bindings, enabled, function, methods, parameters]
6
+ targets: {self-hosted: compatible, aws: refused, vercel: refused, cloudflare: refused, static: refused}
7
+ routes: 1
8
+ grants:
9
+ - kind: env
10
+ description: The exact name DATA_DIR granted to /notes/{name} in an operator policy outside the project (urlcode permissions prints the proposal). The process environment supplies its value.
11
+ files: [urlcode.yaml, functions/note.mjs, data/welcome.txt, tests/requests.json, README.md]
12
+ tests:
13
+ fixtures: tests/requests.json
14
+ commands:
15
+ - urlcode permissions --project examples/data-dir > /path/outside/project/policy.json
16
+ - DATA_DIR=./examples/data-dir/data urlcode validate --local --project examples/data-dir --policy /path/outside/project/policy.json
17
+ - DATA_DIR=./examples/data-dir/data urlcode test --project examples/data-dir --policy /path/outside/project/policy.json
18
+ - DATA_DIR=./examples/data-dir/data urlcode audit --project examples/data-dir --expect-routes 1 --policy /path/outside/project/policy.json
19
+ behavior:
20
+ - GET /notes/welcome answers 200 with the text of $DATA_DIR/welcome.txt
21
+ - an unknown or malformed name answers 404 or 400 and never reaches outside the directory
22
+ - without a policy grant, or with DATA_DIR unset, the project refuses to activate
@@ -0,0 +1,18 @@
1
+ import {readFile} from 'node:fs/promises';
2
+ import {join} from 'node:path';
3
+
4
+ const safeName = /^[a-z0-9-]{1,64}$/;
5
+
6
+ // Trusted function: `env.DATA_DIR` is the operator-resolved {env: DATA_DIR}
7
+ // binding, so the host chooses the directory and this code never reads
8
+ // process.env. The name is still request data: validate it before joining.
9
+ export default async function note(request, {args, env}) {
10
+ if (!safeName.test(args.name)) return Response.json({error: 'unknown note'}, {status: 404});
11
+ try {
12
+ const text = await readFile(join(env.DATA_DIR, `${args.name}.txt`), 'utf8');
13
+ return Response.json({name: args.name, text: text.trimEnd()});
14
+ } catch (error) {
15
+ if (error.code === 'ENOENT') return Response.json({error: 'unknown note'}, {status: 404});
16
+ throw error;
17
+ }
18
+ }
@@ -0,0 +1,6 @@
1
+ [
2
+ {"path":"/notes/welcome","status":200,"expectBody":"{\"name\":\"welcome\",\"text\":\"hello from the data directory\"}"},
3
+ {"path":"/notes/welcome","method":"HEAD","status":200,"expectBody":""},
4
+ {"path":"/notes/missing","status":404},
5
+ {"path":"/notes/..%2Fsecret","status":400}
6
+ ]
@@ -0,0 +1,6 @@
1
+ version: "1"
2
+ routes:
3
+ /notes/{name}:
4
+ function: functions/note.mjs
5
+ env:
6
+ DATA_DIR: {env: DATA_DIR}
@@ -0,0 +1,15 @@
1
+ # Create, read, update, restart
2
+
3
+ One ordered fixture in `tests/requests.json` walks a note through its whole life:
4
+
5
+ ```sh
6
+ urlcode test --project examples/lifecycle
7
+ ```
8
+
9
+ The fixture is a `steps` list. The POST captures the new note's `id` (from the JSON
10
+ body) and its `Location` (from a header); later steps use them as `{{id}}` and
11
+ `{{where}}`. The `{"restart": true}` step closes the runtime and starts it again on
12
+ the same project and data directory, so the last request proves the note survived.
13
+ The function keeps notes under `URLCODE_DATA_DIR`, which `urlcode test` points at a
14
+ fresh temporary directory for each run. All data is synthetic. See the
15
+ [fixture reference](../../docs/READINESS.md#multi-step-fixtures).
@@ -0,0 +1,19 @@
1
+ id: lifecycle
2
+ description: Create, read, update, restart and read again, driven by one ordered fixture with captured values and a restart step, with no hand-written test harness.
3
+ tags: [fixtures, steps, capture, restart, lifecycle, persistence, data-directory, testing, function]
4
+ complexity: intermediate
5
+ capabilities: [bindings, enabled, function, methods, parameters, request.body]
6
+ targets: {self-hosted: compatible, aws: refused, vercel: refused, cloudflare: refused, static: refused}
7
+ routes: 2
8
+ grants:
9
+ - kind: env
10
+ description: URLCODE_DATA_DIR, the data directory. `urlcode test` and `urlcode audit` grant it themselves for their own throwaway directory; a real deployment needs an operator policy grant and a directory that outlives the process.
11
+ files: [urlcode.yaml, functions/notes.mjs, tests/requests.json, README.md]
12
+ tests:
13
+ fixtures: tests/requests.json
14
+ commands:
15
+ - node src/cli.ts test --project examples/lifecycle
16
+ - node src/cli.ts audit --project examples/lifecycle --expect-routes 2
17
+ behavior:
18
+ - a note created by POST /notes is readable, replaceable and still present after the runtime restarts on the same data directory
19
+ - the id and Location the create response returns are captured and substituted into later steps as {{id}} and {{where}}
@@ -0,0 +1,29 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { readFile, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+
5
+ // Trusted first-party code: it writes to the data directory.
6
+ // The directory comes from a declared env binding; `urlcode test` gives every run a fresh one.
7
+ const file = (env, id) => join(env.DATA_DIR, `note-${id}.json`);
8
+
9
+ export async function create(request, { env }) {
10
+ const { text } = await request.json();
11
+ const id = randomBytes(4).toString('hex');
12
+ await writeFile(file(env, id), JSON.stringify({ id, text, version: 1 }));
13
+ return new Response(JSON.stringify({ id, text, version: 1 }), {
14
+ status: 201, headers: { 'content-type': 'application/json', location: `/notes/${id}` },
15
+ });
16
+ }
17
+
18
+ export async function note(request, { args, env }) {
19
+ // The id becomes part of a file name: accept only what create() issues.
20
+ if (!/^[0-9a-f]{8}$/.test(args.id)) return Response.json({ error: 'not found' }, { status: 404 });
21
+ let current;
22
+ try { current = JSON.parse(await readFile(file(env, args.id), 'utf8')); }
23
+ catch { return Response.json({ error: 'not found' }, { status: 404 }); }
24
+ if (request.method === 'PUT') {
25
+ current = { ...current, text: (await request.json()).text, version: current.version + 1 };
26
+ await writeFile(file(env, args.id), JSON.stringify(current));
27
+ }
28
+ return Response.json(current);
29
+ }
@@ -0,0 +1,12 @@
1
+ [
2
+ {"path":"/notes/deadbeef","status":404},
3
+ {"steps":[
4
+ {"path":"/notes","method":"POST","headers":{"content-type":"application/json"},"body":"{\"text\":\"first\"}","status":201,"expectHeaders":{"content-type":"application/json"},
5
+ "capture":{"id":{"json":"id"},"where":{"header":"location"}}},
6
+ {"path":"{{where}}","status":200,"expectBody":"{\"id\":\"{{id}}\",\"text\":\"first\",\"version\":1}"},
7
+ {"path":"/notes/{{id}}","method":"PUT","headers":{"content-type":"application/json"},"body":"{\"text\":\"second\"}","status":200,
8
+ "expectBody":"{\"id\":\"{{id}}\",\"text\":\"second\",\"version\":2}"},
9
+ {"restart":true},
10
+ {"path":"/notes/{{id}}","status":200,"expectBody":"{\"id\":\"{{id}}\",\"text\":\"second\",\"version\":2}"}
11
+ ]}
12
+ ]
@@ -0,0 +1,29 @@
1
+ version: "1"
2
+ routes:
3
+ /notes:
4
+ description: Create a note; the response carries its id and Location
5
+ sandboxReason: Reviewed first-party code; it writes to the data directory, so it is trusted deliberately.
6
+ methods: [POST]
7
+ request:
8
+ body: {required: true, maxBytes: 1024, contentTypes: [application/json]}
9
+ function: {source: functions/notes.mjs, export: create}
10
+ env:
11
+ DATA_DIR: {env: URLCODE_DATA_DIR}
12
+ /notes/{id}:
13
+ description: Read or replace one note, kept in a file under the data directory
14
+ sandboxReason: Reviewed first-party code; it writes to the data directory, so it is trusted deliberately.
15
+ methods: [GET, PUT]
16
+ parameters:
17
+ - name: id
18
+ in: path
19
+ required: true
20
+ schema: {type: string, minLength: 8, maxLength: 8}
21
+ request:
22
+ body: {maxBytes: 1024, contentTypes: [application/json]}
23
+ function:
24
+ source: functions/notes.mjs
25
+ export: note
26
+ args:
27
+ id: {from: path, name: id}
28
+ env:
29
+ DATA_DIR: {env: URLCODE_DATA_DIR}
@@ -0,0 +1,10 @@
1
+ # Custom 404 page
2
+
3
+ `site.notFound` names an HTML file that answers every GET or HEAD matching no
4
+ route, with status 404 and `text/html`. See
5
+ [site conventions](../../docs/SITE.md#notfound--404html).
6
+
7
+ ```sh
8
+ node src/cli.ts test --project examples/not-found
9
+ node src/cli.ts build --target static --project examples/not-found # writes 404.html
10
+ ```
@@ -0,0 +1,17 @@
1
+ id: not-found
2
+ description: A branded 404 page for unmatched routes with one site.notFound key, served with status 404 and buildable as 404.html for static hosting.
3
+ tags: ['404', not-found, error-page, site, html, static]
4
+ complexity: starter
5
+ capabilities: [enabled, methods, page]
6
+ targets: {self-hosted: compatible, aws: compatible, vercel: compatible, cloudflare: refused, static: compatible}
7
+ routes: 2
8
+ files: [urlcode.yaml, public/index.html, public/404.html, tests/requests.json, README.md]
9
+ tests:
10
+ fixtures: tests/requests.json
11
+ commands:
12
+ - node src/cli.ts validate --local --project examples/not-found
13
+ - node src/cli.ts test --project examples/not-found
14
+ - node src/cli.ts audit --project examples/not-found --expect-routes 2
15
+ behavior:
16
+ - an unmatched GET or HEAD is answered with the page and status 404
17
+ - other methods keep the plain-text 404
@@ -0,0 +1,3 @@
1
+ <!doctype html>
2
+ <html lang="en"><head><meta charset="utf-8"><title>Page not found</title></head>
3
+ <body><h1>Page not found</h1><p>That address matches nothing here. <a href="/">Go home</a>.</p></body></html>
@@ -0,0 +1,3 @@
1
+ <!doctype html>
2
+ <html lang="en"><head><meta charset="utf-8"><title>Home</title></head>
3
+ <body><h1>Home</h1><p>Request any other path to see the 404 page.</p></body></html>
@@ -0,0 +1,7 @@
1
+ [
2
+ {"path": "/", "status": 200, "expectHeaders": {"content-type": "text/html; charset=utf-8"}},
3
+ {"path": "/nope", "status": 404, "expectHeaders": {"content-type": "text/html; charset=utf-8", "cache-control": "no-store"}},
4
+ {"path": "/nope", "method": "HEAD", "status": 404, "expectBody": ""},
5
+ {"path": "/nope", "method": "POST", "status": 404, "expectBody": "Not found\n"},
6
+ {"path": "/404.html", "status": 200}
7
+ ]
@@ -0,0 +1,7 @@
1
+ version: "1"
2
+ site:
3
+ notFound: public/404.html
4
+ routes:
5
+ /:
6
+ page:
7
+ file: public/index.html
@@ -0,0 +1,11 @@
1
+ # Shared request and response.headers blocks
2
+
3
+ ```sh
4
+ urlcode test --project examples/shared-blocks
5
+ urlcode routes --project examples/shared-blocks
6
+ ```
7
+
8
+ `shared` names reusable `request` and `response.headers` blocks; a route picks
9
+ one with `use`. `/orders/preview` overrides `request` and keeps the shared
10
+ headers; `/health` overrides `response` and drops the shared headers. `routes`
11
+ prints the resolved result. See [the specification](../../docs/SPECIFICATION.md).
@@ -0,0 +1,17 @@
1
+ id: shared-blocks
2
+ description: Top-level shared request and response.headers blocks that routes select by name with use, resolved at load time.
3
+ tags: [shared, use, reuse, request, response.headers, respond, deduplicate]
4
+ complexity: starter
5
+ capabilities: [enabled, methods, request.body, respond, response.headers]
6
+ targets: {self-hosted: compatible, aws: compatible, vercel: compatible, cloudflare: compatible, static: refused}
7
+ routes: 4
8
+ grants: []
9
+ files: [urlcode.yaml, tests/requests.json, README.md]
10
+ tests:
11
+ fixtures: tests/requests.json
12
+ commands:
13
+ - node src/cli.ts validate --local --project examples/shared-blocks
14
+ - node src/cli.ts test --project examples/shared-blocks
15
+ - node src/cli.ts audit --project examples/shared-blocks --expect-routes 4
16
+ behavior:
17
+ - a route's own request or response key replaces the shared block whole; nothing is deep merged
@@ -0,0 +1,7 @@
1
+ [
2
+ {"path":"/orders","method":"POST","headers":{"content-type":"application/json"},"body":"{\"a\":1}","status":200,"expectBody":"{\"accepted\":true}","expectHeaders":{"cache-control":"no-store","x-api-version":"1"}},
3
+ {"path":"/orders","method":"POST","status":400},
4
+ {"path":"/orders/preview","method":"POST","headers":{"content-type":"application/json"},"body":"{\"a\":1}","status":200,"expectHeaders":{"cache-control":"no-store","x-api-version":"1"}},
5
+ {"path":"/about","status":200,"expectBody":"About this service","expectHeaders":{"cache-control":"public, max-age=300"}},
6
+ {"path":"/health","status":200,"expectHeaders":{"cache-control":"no-store"}}
7
+ ]
@@ -0,0 +1,37 @@
1
+ version: "1"
2
+ # Named blocks a route selects with `use`. Resolved at load time: `urlcode routes`
3
+ # and `urlcode audit` show each route with the block already applied.
4
+ shared:
5
+ json-api:
6
+ request:
7
+ body: {required: true, maxBytes: 4096, contentTypes: [application/json], format: json}
8
+ response:
9
+ headers:
10
+ Cache-Control: no-store
11
+ X-Api-Version: "1"
12
+ plain-page:
13
+ response:
14
+ headers:
15
+ Cache-Control: public, max-age=300
16
+ routes:
17
+ /orders:
18
+ methods: [POST]
19
+ use: json-api
20
+ respond: {json: {accepted: true}}
21
+ /orders/preview:
22
+ methods: [POST]
23
+ use: json-api
24
+ # The route's own request replaces the shared one as a whole; response.headers still comes from json-api.
25
+ request:
26
+ body: {maxBytes: 512, contentTypes: [application/json]}
27
+ respond: {json: {preview: true}}
28
+ /about:
29
+ use: plain-page
30
+ respond: {text: About this service}
31
+ /health:
32
+ use: plain-page
33
+ # An own response block wins whole: the shared Cache-Control does not leak in.
34
+ response:
35
+ headers:
36
+ Cache-Control: no-store
37
+ respond: {json: {ok: true}}