@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
package/llms-full.txt CHANGED
@@ -1,16 +1,17 @@
1
1
  <!-- Generated by scripts/build-llms-full.ts (npm run docs:llms). Do not edit; edit the source documents. -->
2
- <!-- Consolidated URLCode authoring reference: 16 documents, about 52,663 tokens (estimate: characters / 4). -->
2
+ <!-- Consolidated URLCode authoring reference: 16 documents, about 57,753 tokens (estimate: characters / 4). -->
3
3
 
4
+ <!-- urlcode-current-version:start -->
4
5
  # URLCode
5
6
 
6
7
  > A portable runtime for programmable URL behavior, and the framework that grows
7
8
  > from it: routes in YAML, functions and middleware, then accounts, administration and
8
9
  > stored links as operator-installed extensions. Stable project format
9
- > `version: "1"`. Core is Apache-2.0; this revision is `0.4.0-alpha.2`, which makes
10
- > `function`/`middleware` routes trusted by default with `sandbox: true` as the
11
- > per-route opt-in; `0.4.0-alpha.1` is the newest alpha published to npm, on top of
12
- > the `0.3.0` release. The auth, admin and ui extension packages are on npm as
13
- > `0.1.0-alpha.x`, source-complete, review pending.
10
+ > `version: "1"`. Core is Apache-2.0; this revision is `0.4.6`. `function`/`middleware`
11
+ > routes are trusted by default with `sandbox: true` as the per-route opt-in. The
12
+ > auth, admin and ui extension packages in this repository are versioned at the same
13
+ > revision; confirm what is published with `npm run release:status`.
14
+ <!-- urlcode-current-version:end -->
14
15
 
15
16
  Use the schema and docs from the runtime revision you run. Do not assume Node
16
17
  or fetch inside a `sandbox: true` function, regex routes, database access,
@@ -59,10 +60,12 @@ claim here is implemented in the linked repository; nothing is roadmap.
59
60
  | `@jimhoyd/urlcode-auth` | [`packages/auth`](https://github.com/jimhoyd-com/urlcode/blob/main/packages/auth) | Accounts: password, passkeys, OpenID Connect, email codes, TOTP, recovery, sessions, roles, registration modes, account page, operator CLI | `extensions.auth` plus an `/account/*` mount and `policies.extensions.auth` on protected routes |
60
61
  | `@jimhoyd/urlcode-admin` | [`packages/admin`](https://github.com/jimhoyd-com/urlcode/blob/main/packages/admin) | Administration: users, sessions, roles, audit, registration approval, two-person cases, support impersonation, health | `extensions.admin` plus an `/admin/*` mount |
61
62
 
62
- All four are Apache-2.0. The `0.4.1` release line aligns their stable versions;
63
- check `npm run release:status` for publication progress. A stable npm channel is
64
- not an independent assessment: review, deployment evidence and an accessibility
65
- assessment are still pending
63
+ All four are Apache-2.0. Each package's npm `latest` tag identifies its stable
64
+ version. The current publisher records the exact four-package stack tested
65
+ together on each new GitHub release and attaches the same information in signed
66
+ `train.json` metadata.
67
+ A stable npm channel is not an independent assessment: review, deployment
68
+ evidence and an accessibility assessment are still pending
66
69
  ([issue 58](https://github.com/jimhoyd-com/urlcode/issues/58)). Their status
67
70
  files say exactly what is built: [auth](https://github.com/jimhoyd-com/urlcode/blob/main/packages/auth/IMPLEMENTATION-STATUS.md),
68
71
  [admin](https://github.com/jimhoyd-com/urlcode/blob/main/packages/admin/IMPLEMENTATION-STATUS.md),
@@ -115,11 +118,12 @@ exact requirement.
115
118
 
116
119
  ### The composition contract
117
120
 
118
- After the aligned `0.4.1` packages are published, an extended project starts
119
- with the packages and one command:
121
+ An extended project starts from the current stable packages and one command.
122
+ `--save-exact` records the concrete versions selected from the independent
123
+ `latest` channels:
120
124
 
121
125
  ```sh
122
- npm install --save-exact @jimhoyd/urlcode@0.4.1 @jimhoyd/urlcode-ui@0.4.1 @jimhoyd/urlcode-auth@0.4.1 @jimhoyd/urlcode-admin@0.4.1
126
+ npm install --save-exact @jimhoyd/urlcode@latest @jimhoyd/urlcode-ui@latest @jimhoyd/urlcode-auth@latest @jimhoyd/urlcode-admin@latest
123
127
  urlcode init my-site --with ui,auth,admin
124
128
  ```
125
129
 
@@ -178,12 +182,22 @@ export default {
178
182
  };
179
183
  ```
180
184
 
181
- Auth and admin already render their screens through `ui.kit` when the host
182
- supplies the UI extension; their package-owned templates and catalogues must be
183
- registered with that kit. Without it, both retain shared primitive rendering
184
- through `presentation`. The example above uses that primitive fallback. The UI
185
- block is optional, and a migration to the kit is not unfinished framework work.
186
- See each package README for its complete kit registration example.
185
+ Auth and admin render every screen through `ui.kit`; their package-owned
186
+ templates and catalogues must be registered with that kit. Both refuse
187
+ activation when the UI extension is absent or has not activated first. See each
188
+ package README for the complete registration example; the scaffold generated by
189
+ `urlcode init --with ui,auth,admin` wires the same composition.
190
+
191
+ Treat that composition as one application with package ownership boundaries,
192
+ not as three separate user interfaces. Keep identity, session and recovery
193
+ behavior in auth and authorization, freshness, auditing and mutations in admin.
194
+ Apply product differences through the installed extensions' declared authoring
195
+ surfaces, in this order: configuration and theme, copy, a component or screen
196
+ template, stylesheet, then a supported hook. Create another extension only for
197
+ a reusable capability those surfaces cannot express. `urlcode extensions`
198
+ and the MCP `get_extensions` tool report those surfaces and their fast checks,
199
+ so people and agents can discover the supported path instead of replacing
200
+ package behavior.
187
201
 
188
202
  ```sh
189
203
  urlcode serve --project /absolute/site --host-file /absolute/operator/host.mjs --origin https://site.example
@@ -220,6 +234,12 @@ These are the facts that keep generated projects valid. The full matrix is in
220
234
  Function and middleware `source` fields do name project modules. Extensions are
221
235
  logical names; the host file chooses the implementation. There is no
222
236
  `--extension` flag, no `import` in YAML, no interpolation.
237
+ - **Build one product through declared authoring surfaces.** Inspect extension
238
+ authoring metadata before generating code. Prefer configuration/theme, copy,
239
+ the smallest template override, stylesheet and supported hooks, in that
240
+ order. Add an extension only for a reusable missing capability. Run the
241
+ reported fast checks while iterating and the full repository checks before
242
+ handoff.
223
243
  - **One handler per route.** `redirect`, `respond`, `page`, `static`, `download`,
224
244
  `function`, `proxy`, `conditional` or `extension`, plus optional
225
245
  `middleware`. Paths are exact or single-segment `{param}`; `/*` only on
@@ -258,7 +278,7 @@ These are the facts that keep generated projects valid. The full matrix is in
258
278
  | Add accounts | [auth README](https://github.com/jimhoyd-com/urlcode/blob/main/packages/auth/README.md), [auth security](https://github.com/jimhoyd-com/urlcode/blob/main/packages/auth/SECURITY.md) |
259
279
  | Add administration | [admin README](https://github.com/jimhoyd-com/urlcode/blob/main/packages/admin/README.md) |
260
280
  | Restyle every page | [ui README](https://github.com/jimhoyd-com/urlcode/blob/main/packages/ui/README.md), [ui contract](https://github.com/jimhoyd-com/urlcode/blob/main/packages/ui/CONTRACT.md) |
261
- | Write an extension | [extensions](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md), [extension model review](https://github.com/jimhoyd-com/urlcode/blob/main/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md) |
281
+ | Write an extension | [extensions](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md) |
262
282
  | Run it | [operations](https://github.com/jimhoyd-com/urlcode/blob/main/docs/OPERATIONS.md), [install](https://github.com/jimhoyd-com/urlcode/blob/main/docs/INSTALL.md), [deployment checks](https://github.com/jimhoyd-com/urlcode/blob/main/docs/DEPLOYMENT-CHECKS.md) |
263
283
 
264
284
  ---
@@ -267,12 +287,29 @@ These are the facts that keep generated projects valid. The full matrix is in
267
287
 
268
288
  Source: https://github.com/jimhoyd-com/urlcode/blob/main/docs/AI-AUTHORING.md
269
289
 
270
- Use this as project-authoring context. It describes the implemented source contract, including unreleased additions after 0.3.0,
290
+ Use this as project-authoring context. It describes the implemented source contract, including additions since 0.3.0,
271
291
  not a general server framework. Runtime/schema/docs
272
292
  must come from the same reviewed revision. The runtime is Apache-2.0; a
273
293
  project you generate carries whatever license its owner chooses, so do not
274
294
  add one to it automatically.
275
295
 
296
+ ### First step: one bounded query
297
+
298
+ Make the first retrieval one bounded query: the MCP tool `get_context` when the
299
+ `urlcode` server is registered, otherwise `urlcode context --project DIR` (add
300
+ `--budget N` when context is scarce). Then retrieve only what the task needs:
301
+ `urlcode capabilities NAME` (MCP `get_capability`) for one capability's limits,
302
+ `get_schema` for one YAML fragment, `urlcode recipes search TEXT`
303
+ (`search_recipes`), `explain` for a route's effective behavior and, when the
304
+ operator supplies a host file, `get_extensions`. Context is a summary with the
305
+ constraints and exact commands, not a schema dump, and it never hides a
306
+ capability limit: ask `capabilities NAME` before promising a feature.
307
+
308
+ The complete catalogs (`urlcode capabilities`, `recipes list`), the compact
309
+ [llms.txt](https://github.com/jimhoyd-com/urlcode/blob/main/llms.txt) index and the generated [llms-full.txt](https://github.com/jimhoyd-com/urlcode/blob/main/llms-full.txt)
310
+ stay available as deliberate fallback and reference, not as the opening move.
311
+ The reading order below is for that reference use.
312
+
276
313
  ### Declarative-first default
277
314
 
278
315
  > Use URLCode's highest-level declarative features whenever possible. Generate custom code only when the framework cannot express the requirement.
@@ -316,18 +353,31 @@ as evidence, and the rules on grants and secrets. Assistants that load skills
316
353
  find the same loop in `skills/urlcode/SKILL.md` inside the installed package; it
317
354
  teaches how to retrieve the minimum reference through `urlcode capabilities`,
318
355
  `urlcode recipes list|show` and `urlcode validate --local` rather than reading
319
- the documentation whole. Neither file replaces the schema; both defer to it.
356
+ the documentation whole. For a host-composed application, `get_extensions`
357
+ adds each extension's schemas, hooks, supported authoring surfaces and fast
358
+ checks. Agents should use those surfaces before generating replacement package
359
+ behavior. Neither file replaces the schema; both defer to it.
360
+
361
+ Treat core, installed extensions and product UI as one application with
362
+ different owners. Keep auth/admin security and workflow behavior package-owned;
363
+ keep branding, product navigation and the smallest necessary overrides in the
364
+ project. When a React frontend contains `components.json`, use the installed
365
+ official shadcn/ui skill for component discovery, composition, accessibility
366
+ and semantic Tailwind styling: start with `shadcn info --json`, then use its
367
+ documentation/search flow or configured MCP registry before generating a
368
+ component. The server template kit is shadcn-compatible but does not accept
369
+ React components. See the official [shadcn/ui skills guide](https://ui.shadcn.com/docs/skills).
320
370
 
321
371
  ### Authoring workflow
322
372
 
323
- Run `urlcode context --project ./my-links` first. It prints, in a few hundred
324
- tokens, the runtime and schema version, what the project already uses, the
373
+ Start with the bounded query above. It prints the runtime and schema version, what the project already uses, the
325
374
  constraints that hold for every project, which targets refuse this project's
326
375
  features and the exact validate, test and audit commands with the intentional
327
376
  route count filled in. It is derived from the compiled project and the
328
377
  capability catalog, never from prose, so prefer it to re-reading the
329
378
  documentation; add `--budget N` when context is scarce and `--json` for
330
- tooling. The same data is available from the MCP tool `get_context`.
379
+ tooling. Its size grows with the project (about a thousand estimated tokens for
380
+ the starter, a few thousand for the cookbook), not with the framework.
331
381
 
332
382
  - Inspect the existing entry point, included files, functions, tests and pinned
333
383
  runtime. Preserve the user's organization and unrelated routes.
@@ -361,6 +411,26 @@ Use the intentional actual count, not always 2. Runtime checkout users can repla
361
411
  External bindings require an already reviewed policy; add `--policy` where needed.
362
412
  The benchmark operates locally; it is not a load test of an external deployment.
363
413
 
414
+ ### Feedback from real authoring work
415
+
416
+ The authoring loop is also a source of roadmap evidence. After completing a
417
+ real task, an assistant should make a feedback **draft** only when a concrete
418
+ attempt exposes a capability gap, repeated framework workaround,
419
+ documentation/discovery gap, or suspected defect. One-off product logic is not
420
+ framework feedback.
421
+
422
+ The draft must name the installed runtime version and requested target, then
423
+ include a sanitized route/YAML fragment, the exact validation or test
424
+ observation, the smallest expected behavior, and a proposed fixture. It must
425
+ not include secrets, customer URLs, raw application source, or unsupported
426
+ claims inferred from a failed attempt.
427
+
428
+ Search [existing URLCode issues](https://github.com/jimhoyd-com/urlcode/issues)
429
+ before proposing a new one and identify a likely duplicate when found. An agent
430
+ may present a draft issue or comment for review, but must never create or update
431
+ a GitHub issue without the user's explicit approval. Feedback is evidence for a
432
+ maintainer to review; it is not a promise that the public contract will grow.
433
+
364
434
  ### Capability matrix: do not hallucinate these features
365
435
 
366
436
  | Available | Unavailable or future |
@@ -406,11 +476,49 @@ declared route at the same path still wins. Count its generated routes in
406
476
  `--expect-routes`. `site.sitemap` needs `--origin` at every command that
407
477
  activates the project; see [site conventions](https://github.com/jimhoyd-com/urlcode/blob/main/docs/SITE.md).
408
478
 
479
+ ### Built-in features by task
480
+
481
+ Before writing a function, check whether a declarative feature already covers the
482
+ need. Security headers are the usual miss: a project that declares nothing sends
483
+ only the runtime's defaults (`nosniff`, `no-store`, a request ID).
484
+
485
+ | I need | Declare | Reference |
486
+ |---|---|---|
487
+ | Security headers (CSP, HSTS, frame and referrer policy) | `policies.security: {headers: oshp}` or `policies.profile: hardened` | [security](https://github.com/jimhoyd-com/urlcode/blob/main/docs/policies/security.md) |
488
+ | Cache headers on a page, download or static mount | `cacheControl`: `no-cache` (default), `no-store`, `public, max-age=3600` or `public, max-age=31536000, immutable`; nothing else validates | [assets](https://github.com/jimhoyd-com/urlcode/blob/main/docs/yaml/assets.md) |
489
+ | A cache strategy on any route | `policies.cache` | [cache](https://github.com/jimhoyd-com/urlcode/blob/main/docs/policies/cache.md) |
490
+ | Body size, required body, content types, JSON syntax and shape | `request.body.maxBytes`, `required`, `contentTypes`, `format`, `schema` | [HTTP](https://github.com/jimhoyd-com/urlcode/blob/main/docs/HTTP.md#body-schema-and-input-patterns) |
491
+ | A uuid path id or a bounded string pattern | parameter `schema: {type: string, format: uuid}` or `pattern` with `maxLength` | [HTTP](https://github.com/jimhoyd-com/urlcode/blob/main/docs/HTTP.md#body-schema-and-input-patterns) |
492
+ | Method gating | `methods` (default GET/HEAD; 405 with `Allow`) | [HTTP](https://github.com/jimhoyd-com/urlcode/blob/main/docs/HTTP.md) |
493
+ | Rate limits, bot and crawler denial, compression | `policies.throttle`, `agents`, `compression` | [policies](https://github.com/jimhoyd-com/urlcode/blob/main/docs/POLICIES.md) |
494
+ | Static JSON or text and fixed headers | `respond`, `response.headers` | [HTTP](https://github.com/jimhoyd-com/urlcode/blob/main/docs/HTTP.md) |
495
+ | robots, sitemap, favicon, security.txt, llms.txt | top-level `site` | [site](https://github.com/jimhoyd-com/urlcode/blob/main/docs/SITE.md) |
496
+
497
+ Which handler serves the response:
498
+
499
+ | The response is | Handler | Recipe |
500
+ |---|---|---|
501
+ | Fixed text or JSON | `respond` | `health-page` |
502
+ | A short HTML snippet | `respond` `text` plus `response.headers` `Content-Type: text/html; charset=utf-8` | [HTTP](https://github.com/jimhoyd-com/urlcode/blob/main/docs/HTTP.md) |
503
+ | One HTML file | `page` | `static-page` |
504
+ | A directory of files | `static` | `static-plus-api` |
505
+ | An attachment | `download` | `protected-download` |
506
+
507
+ Data persistence has no native handler. The operator-installed `store` extension
508
+ serves declared collections as a CRUD API, and `urlcode recipes search "crud store
509
+ persist"` finds the `store-crud` recipe. It needs the operator to install
510
+ `@jimhoyd/urlcode-store` (on npm) and a host file. `init --with
511
+ ui,auth,store` scaffolds one from the published packages; a no-auth
512
+ `--with store` needs `--ack store:public-write`, which only a core release after the
513
+ store's first publication has, so say so rather than promising it. Report anything beyond that recipe (filtering, sorting, per-record
514
+ ownership, a database) as a gap. `urlcode context` lists the same built-ins so
515
+ they are visible before you write code.
516
+
409
517
  ### Agent skills
410
518
 
411
519
  This repository ships two agent skills, each a thin trigger pointing at the
412
- docs that are the actual source of truth, so there is one place to keep
413
- current rather than two:
520
+ version-matched CLI/schema and documentation bundle that are the actual source
521
+ of truth, so there is one place to keep current rather than two:
414
522
 
415
523
  - [`urlcode-authoring`](https://github.com/jimhoyd-com/urlcode/blob/main/.claude/skills/urlcode-authoring/SKILL.md) — this
416
524
  guide, the schema and the reference. It loads the capability limits and the
@@ -431,7 +539,10 @@ Three ways to get either, all pinned to a runtime revision:
431
539
  - **npm.** The published package includes both skill directories. Copy the
432
540
  one(s) you want into your project's `.claude/skills/` to pin guidance to the
433
541
  same revision as the runtime you installed; a skill inside `node_modules` is
434
- not discovered on its own.
542
+ not discovered on its own. The npm archive keeps `llms-full.txt` as its one
543
+ offline prose bundle instead of duplicating the repository's `docs/` tree;
544
+ use the CLI for structured queries and search that bundle by document heading
545
+ when more explanation is needed.
435
546
  - **Plugin marketplace.** `.claude-plugin/marketplace.json` publishes the
436
547
  `packaging/claude-plugin` distribution from this repository, carrying both
437
548
  skills. Add the marketplace by its Git URL and install the `urlcode` plugin.
@@ -441,7 +552,7 @@ Three ways to get either, all pinned to a runtime revision:
441
552
 
442
553
  `npm run docs:plugin` regenerates the plugin distribution from both skills;
443
554
  `npm run check` fails if it is stale or if either skill names a documentation
444
- path this revision does not ship.
555
+ path that does not exist in this revision.
445
556
  ### Bounded authoring tools
446
557
 
447
558
  Before generating a common route by hand, search the bundled catalog:
@@ -536,13 +647,18 @@ under `advisories` with "consider whether this route needs `sandbox: true`".
536
647
  This is a nudge to look, the same advisory spirit as the rest of `audit`'s
537
648
  non-blocking findings — it never fails the check, never sets `ready: false`
538
649
  and never infers the actual answer; setting `sandboxReason` (with `sandbox`
539
- either `true` or `false`) or `sandbox: true` is enough to silence it.
650
+ either `true` or `false`) or `sandbox: true` is enough to silence it. The
651
+ advisory prints the exact line to add. Anything that touches the filesystem
652
+ (a persistent app writing files, for example) must be a trusted route,
653
+ because a sandbox has no filesystem: declare `sandboxReason` with the default
654
+ `sandbox: false` and say why it is trusted, as the `static-plus-api` recipe does.
540
655
 
541
656
  The same judgment call applies to a project-level lifecycle hook an
542
657
  extension invokes (`onSignUp`, `beforeRegister` and the like) — it is
543
- first-party project code with the same trusted-by-default rule and the same
544
- `sandbox: true` opt-in as any `function`/`middleware` route, no special
545
- case. See [EXTENSIONS.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md#project-level-lifecycle-hooks).
658
+ first-party project code with the same trusted-by-default rule as a
659
+ `function`/`middleware` route. Extension hook contract v1 is trusted-only;
660
+ `sandbox: true` is rejected rather than silently ignored. See
661
+ [EXTENSIONS.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md#project-level-lifecycle-hooks).
546
662
 
547
663
  Guest TypeScript needs `build-typescript --project SOURCE --out NEW_DIRECTORY`
548
664
  before serving. Only the emitted `.js`/`.mjs` executes, in QuickJS for a
@@ -561,7 +677,7 @@ origin grants pinned to the project revision. These are self-hosted features;
561
677
  providers refuse them. Signals are bounded best effort with drops, no retries
562
678
  or persistence. Never turn a user request into an implicit network grant.
563
679
 
564
- Before using a feature, ask `urlcode capabilities <name>` for its constraints, grants and target support and `urlcode schema <path>` for only that YAML fragment (MCP: `get_capability`, `get_schema`), instead of guessing.
680
+ Before using a feature, ask `urlcode capabilities <name>` for its constraints, grants and target support and `urlcode schema <path>` for only that YAML fragment (MCP: `get_capability`, `get_schema`), instead of guessing. For an installed extension, use `urlcode extensions --host-file ... --json` or MCP `get_extensions`; its hook contracts include the accepted names, purpose and input/output schemas. Prefer extension configuration and UI copy/templates/theme/CSS, then a declared project hook, and only then a new extension or fork.
565
681
  The [tooling SDK and stdio MCP](https://github.com/jimhoyd-com/urlcode/blob/main/docs/TOOLING.md) inspect, validate, explain and preview
566
682
  without guest execution, environment reads or writes. Run `urlcode explain /route`
567
683
  to check effective methods, policies and cache outcome, and `urlcode manifest`
@@ -610,10 +726,12 @@ programmatic compatibility analysis and provider verification limits.
610
726
 
611
727
  Source: https://github.com/jimhoyd-com/urlcode/blob/main/docs/YAML-GUIDE.md
612
728
 
613
- This guide targets URLCode 0.3.0. Start with the function example below,
729
+ <!-- urlcode-current-version:start -->
730
+ This guide targets URLCode 0.4.6. Start with the function example below,
614
731
  then add only the fields your route needs. The authoritative machine-readable
615
732
  shape is [JSON Schema](https://github.com/jimhoyd-com/urlcode/blob/main/schemas/urlcode.schema.json); semantic rules are in the
616
733
  [specification](https://github.com/jimhoyd-com/urlcode/blob/main/docs/SPECIFICATION.md). Unsupported fields fail validation.
734
+ <!-- urlcode-current-version:end -->
617
735
 
618
736
  ### Run all the examples
619
737
 
@@ -693,6 +811,8 @@ schema-valid combinations activate successfully.
693
811
  | `routes.*.enabled` | boolean | no | — |
694
812
  | `routes.*.sandbox` | boolean | no | default: false |
695
813
  | `routes.*.sandboxReason` | string | no | maxLength: 500 |
814
+ | `routes.*.coveredElsewhere` | object | no | minProperties: 1 |
815
+ | `routes.*.coveredElsewhere.*` | string | no | minLength: 1; maxLength: 500 |
696
816
  | `routes.*.expires` | string | no | — |
697
817
  | `routes.*.description` | string | no | maxLength: 1024 |
698
818
  | `routes.*.parameters` | array | no | maxItems: 64 |
@@ -710,6 +830,8 @@ schema-valid combinations activate successfully.
710
830
  | `routes.*.parameters[].schema.maxLength` | integer | no | minimum: 0; maximum: 8192 |
711
831
  | `routes.*.parameters[].schema.minimum` | number | no | — |
712
832
  | `routes.*.parameters[].schema.maximum` | number | no | — |
833
+ | `routes.*.parameters[].schema.pattern` | string | no | minLength: 1; maxLength: 128 |
834
+ | `routes.*.parameters[].schema.format` | string | no | enum: ["uuid"] |
713
835
  | `routes.*.parameters[].schema.items` | object | no | unknown keys rejected |
714
836
  | `routes.*.parameters[].schema.items.type` | string | yes | enum: ["string","integer","number","boolean"] |
715
837
  | `routes.*.parameters[].schema.maxItems` | integer | no | minimum: 0; maximum: 100 |
@@ -770,6 +892,7 @@ schema-valid combinations activate successfully.
770
892
  | `routes.*.request.body.contentTypes` | array | no | minItems: 1; maxItems: 16; uniqueItems: true |
771
893
  | `routes.*.request.body.contentTypes[]` | string | no | pattern: "^[a-z0-9!#$&^_.+-]+/[a-z0-9!#$&^_.+-]+$" |
772
894
  | `routes.*.request.body.format` | string | no | enum: ["text","json"] |
895
+ | `routes.*.request.body.schema` | object | no | — |
773
896
  | `routes.*.response` | object | no | unknown keys rejected |
774
897
  | `routes.*.response.headers` | object | no | maxProperties: 64 |
775
898
  | `routes.*.response.headers.*` | one of the shapes below | no | — |
@@ -950,6 +1073,7 @@ schema-valid combinations activate successfully.
950
1073
  | `routes.*.cache.maxBytes` | integer | no | minimum: 0; maximum: 16777216 |
951
1074
  | `routes.*.cache.maxEntries` | integer | no | minimum: 1; maximum: 1000000 |
952
1075
  | `routes.*.cache.force` | boolean | no | default: false |
1076
+ | `routes.*.use` | string | no | pattern: "^[a-z][a-z0-9-]{0,63}$" |
953
1077
  | `includes` | array | no | maxItems: 256; uniqueItems: true |
954
1078
  | `includes[]` | string | no | maxLength: 1024 |
955
1079
  | `policies` | object | no | unknown keys rejected |
@@ -1114,10 +1238,27 @@ schema-valid combinations activate successfully.
1114
1238
  | `site.securityTxt.encryption` | array | no | maxItems: 64 |
1115
1239
  | `site.securityTxt.encryption[]` | string | no | minLength: 1; maxLength: 2048 |
1116
1240
  | `site.llms` | string | no | minLength: 1; maxLength: 1024 |
1241
+ | `site.notFound` | string | no | minLength: 1; maxLength: 1024; pattern: "\\.[hH][tT][mM][lL]?$" |
1117
1242
  | `extensions` | object | no | maxProperties: 16 |
1118
1243
  | `extensions.*` | object | no | unknown keys rejected |
1119
1244
  | `extensions.*.version` | constant | yes | const: "1" |
1120
1245
  | `extensions.*.config` | object | yes | — |
1246
+ | `shared` | object | no | maxProperties: 32 |
1247
+ | `shared.*` | object | no | unknown keys rejected |
1248
+ | `shared.*.request` | object | no | unknown keys rejected |
1249
+ | `shared.*.request.body` | object | no | unknown keys rejected |
1250
+ | `shared.*.request.body.required` | boolean | no | — |
1251
+ | `shared.*.request.body.maxBytes` | integer | no | minimum: 0; maximum: 1048576 |
1252
+ | `shared.*.request.body.contentTypes` | array | no | minItems: 1; maxItems: 16; uniqueItems: true |
1253
+ | `shared.*.request.body.contentTypes[]` | string | no | pattern: "^[a-z0-9!#$&^_.+-]+/[a-z0-9!#$&^_.+-]+$" |
1254
+ | `shared.*.request.body.format` | string | no | enum: ["text","json"] |
1255
+ | `shared.*.request.body.schema` | object | no | — |
1256
+ | `shared.*.response` | object | no | unknown keys rejected |
1257
+ | `shared.*.response.headers` | object | no | maxProperties: 64 |
1258
+ | `shared.*.response.headers.*` | one of the shapes below | no | — |
1259
+ | `shared.*.response.headers.* (option 1)` | string | no | maxLength: 4096 |
1260
+ | `shared.*.response.headers.* (option 2)` | array | no | minItems: 1; maxItems: 16 |
1261
+ | `shared.*.response.headers.* (option 2)[]` | string | no | maxLength: 4096 |
1121
1262
 
1122
1263
  ---
1123
1264
 
@@ -1126,7 +1267,7 @@ schema-valid combinations activate successfully.
1126
1267
  Source: https://github.com/jimhoyd-com/urlcode/blob/main/docs/SPECIFICATION.md
1127
1268
 
1128
1269
  This document and [JSON Schema](https://github.com/jimhoyd-com/urlcode/blob/main/schemas/urlcode.schema.json) describe
1129
- the source contract, including unreleased additions after 0.3.0.
1270
+ the source contract, including additions since 0.3.0.
1130
1271
  `version: "1"` remains the project-format contract. Unsupported fields
1131
1272
  are rejected rather than interpreted as future behavior.
1132
1273
 
@@ -1231,6 +1372,31 @@ Routes accept `request.body` validation and `response.headers` overrides. The
1231
1372
  See [HTTP configuration](https://github.com/jimhoyd-com/urlcode/blob/main/docs/HTTP.md) for the exact supported fields, precedence,
1232
1373
  security restrictions and examples.
1233
1374
 
1375
+ ### Shared blocks
1376
+
1377
+ An optional top-level `shared` map (entry `urlcode.yaml` only; at most 32 names
1378
+ matching `^[a-z][a-z0-9-]{0,63}$`) holds named `request` and `response.headers`
1379
+ blocks. A route selects one with `use: <name>`. This is the reuse mechanism;
1380
+ anchors, aliases and merge keys stay rejected. Rules:
1381
+
1382
+ - A route's own `request` (or `response`) key replaces the shared block's key as
1383
+ a whole. There is no deep merge, so a reviewer never reconstructs a header set
1384
+ from two places.
1385
+ - Resolution happens at load time, before the route hash, `audit`, `routes` and
1386
+ every compiler run. The resolved route carries no `use`, and changing a shared
1387
+ block changes the hash of exactly the routes that select it.
1388
+ - An unknown name fails validation, as does a shared block whose
1389
+ `response.headers` name a header the runtime owns (the same set a route may
1390
+ not set). Included files may `use` names from the entry file but may not
1391
+ declare `shared`. Per-route rules, such as the asset-handler header limits,
1392
+ still apply to the resolved route.
1393
+ - Only `request` and `response.headers` are shareable. `sandbox`,
1394
+ `sandboxReason`, `policies` and other route defaults are deliberately left
1395
+ out for now, so each trust decision stays on its route.
1396
+ - No cross-file or remote reuse of blocks.
1397
+
1398
+ See the [shared-blocks example](https://github.com/jimhoyd-com/urlcode/blob/main/examples/shared-blocks/README.md).
1399
+
1234
1400
  ### Policies
1235
1401
 
1236
1402
  Optional top-level `policies` and `profiles` keys, and `routes.<path>.policies`,
@@ -1253,8 +1419,11 @@ Duplicate scalar query/header inputs return 400. Required missing inputs return
1253
1419
  400; defaults apply only to absent inputs. Empty strings are present values.
1254
1420
 
1255
1421
  Supported validation: `type`, scalar `enum`, `default`, string `minLength`/
1256
- `maxLength`, numeric `minimum`/`maximum`, query array `items` and `maxItems`.
1257
- Regular-expression patterns, structured bodies, cookies, nested inputs and
1422
+ `maxLength`, string `format: uuid` and a bounded `pattern` ([restrictions](https://github.com/jimhoyd-com/urlcode/blob/main/docs/HTTP.md#body-schema-and-input-patterns)),
1423
+ numeric `minimum`/`maximum`, query array `items` and `maxItems`. JSON bodies are
1424
+ validated by `request.body.schema`, not by a parameter (a failure answers 422, as JSON
1425
+ when the client explicitly accepts `application/json` and otherwise as text; see
1426
+ [HTTP](https://github.com/jimhoyd-com/urlcode/blob/main/docs/HTTP.md#body-schema-and-input-patterns)). Cookies, nested inputs and
1258
1427
  OpenAPI `style`/`explode` fields are not implemented. This uses a documented
1259
1428
  OpenAPI-like input subset; it is not an OpenAPI document or full JSON Schema
1260
1429
  input vocabulary. String limits are at most 8,192 characters.
@@ -1327,6 +1496,16 @@ entry may likewise be a string, normalized to `{source: <string>}`. Only the
1327
1496
  long form exists after loading, so `routes`, `audit`, `explain`, revision hashes
1328
1497
  and the field reference describe the expansion.
1329
1498
 
1499
+ Auto-binding belongs to the string form only. The long form (`source`, and
1500
+ therefore any `export:`) is taken as written: URLCode does not add `args` or
1501
+ path `parameters` to it, so a route like `/api/todos/{id}` with
1502
+ `function: {source: ..., export: get}` must declare the `id` parameter under
1503
+ `parameters` and map it under `args` by hand, as the example above does. A
1504
+ function has one `source` and one `export` per route; to serve several methods
1505
+ on one path, branch on `request.method` inside the function, or declare one
1506
+ route per method where the path allows it. A `methods:` map of per-method
1507
+ functions is not implemented.
1508
+
1330
1509
  ES modules only. `.mjs` is loaded as ESM in both modes. A `sandbox: true` route
1331
1510
  also reads `.js` as ESM independently of Node package settings, but a trusted
1332
1511
  route imports `.js` through Node's own loader, so an ESM `.js` module needs the
@@ -1645,6 +1824,7 @@ status or default `Cache-Control: no-store` on functions/redirects.
1645
1824
  | `request.body.maxBytes` | 0–1048576; tighter per-route budget, enforced while reading fixed/chunked bodies; 413 on overflow |
1646
1825
  | `request.body.contentTypes` | Exact lowercase MIME essences for nonempty bodies; parameters ignored; mismatch/missing type returns 415 |
1647
1826
  | `request.body.format` | `text`: validate UTF-8; `json`: validate UTF-8, JSON media type and JSON syntax; malformed input returns 400 |
1827
+ | `request.body.schema` | Requires `format: json`. A JSON Schema subset checked after parsing; a body that breaks it returns 422 (see below) |
1648
1828
 
1649
1829
  The operator request limit remains an upper bound; YAML cannot raise it. A route
1650
1830
  without body policy keeps the existing server limit. A configured body policy
@@ -1684,12 +1864,76 @@ cache and disposition on its own handler. Asset metadata cannot be overridden by
1684
1864
  `response.headers`. On functions/declared responses, Content-Type may be configured;
1685
1865
  JSON declarations require a JSON type. No response header secret interpolation.
1686
1866
 
1867
+ #### Body schema and input patterns
1868
+
1869
+ `request.body.schema` accepts `type` (`object`, `array`, `string`, `integer`,
1870
+ `number`, `boolean`, `null`), `properties`, `required`, `additionalProperties`
1871
+ (true or false), `items`, scalar `enum`, `minLength`/`maxLength`, `pattern`,
1872
+ `format: uuid`, `minimum`/`maximum` and `minItems`/`maxItems`. Anything else,
1873
+ including `$ref`, `oneOf` and `default`, fails activation. A schema is limited to
1874
+ 6 levels, 128 nodes and 64 properties per object. It is checked by the runtime
1875
+ itself, so it behaves the same on every host and is not compiled from author
1876
+ code.
1877
+
1878
+ A failing body answers **422** as `text/plain`: `Request body failed validation`
1879
+ then one line per failure, at most 8, each naming only a path the schema
1880
+ declared (`/title must be a string`). Array positions print as `[]`. Nothing the
1881
+ client sent is echoed, in line with the fixed-words rule for runtime errors.
1882
+ Malformed JSON stays 400 and a wrong media type 415.
1883
+
1884
+ A client that sends `Accept: application/json` gets the same failures as
1885
+ `application/json` instead (the server and the Cloudflare Worker agree):
1886
+
1887
+ ```json
1888
+ {"error":"body_validation_failed","message":"Request body failed validation","issues":[{"pointer":"/title","keyword":"maxLength","message":"must be at most 8 characters","expected":8}]}
1889
+ ```
1890
+
1891
+ Each issue carries `pointer` (RFC 6901, built only from names the schema
1892
+ declared; array positions are `/[]`, not an index; the root is `""`), `keyword`
1893
+ (`type`, `enum`, `required`, `additionalProperties`, `minLength`, `maxLength`,
1894
+ `format`, `pattern`, `minimum`, `maximum`, `minItems` or `maxItems`), the fixed
1895
+ `message`, and where the schema states one, `expected` (the type, bound, format
1896
+ or, for `enum`, up to 16 short declared values) or `property` (the missing name
1897
+ from `required`). The offending value is never included, because it may hold a
1898
+ secret. At most 8 issues are listed and the body is capped at 4096 bytes;
1899
+ when trailing issues are dropped to fit, `"truncated":true` is added.
1900
+
1901
+ Negotiation is deliberately conservative: JSON is sent only when the Accept
1902
+ header names `application/json` explicitly with `q` above 0 and no higher `q` for
1903
+ an explicit `text/plain`. A missing header, `*/*`, `application/*`, browsers'
1904
+ default Accept and a malformed `q` keep the plain-text answer. The status is 422
1905
+ either way and the same checks run before any function or sandbox code.
1906
+
1907
+ Parameter schemas (path, query, header) also accept `format: uuid` and `pattern`
1908
+ on string inputs, rejecting a mismatch with 400. `pattern` runs on every request
1909
+ in the host process, so it is restricted: 1 to 128 characters, `maxLength` of at
1910
+ most 128 on the same schema, no group repeated by `*`, `+` or `{n,}`, no
1911
+ lookaround, no backreference and at most three unbounded quantifiers. That
1912
+ restriction is conservative, not a proof of linear time. It is what stands
1913
+ between an author regex and a backtracking stall, so prefer `format` or `enum`
1914
+ when either fits.
1915
+
1687
1916
  `respond` is an additional native handler (exactly one handler per route):
1688
1917
 
1689
1918
  - `status`: 200–599, default 200; 206 and 304 are reserved for native asset semantics.
1690
1919
  - `text`: literal UTF-8 body, default content type text/plain.
1691
1920
  - `json`: any JSON-compatible YAML value, serialized with application/json.
1692
1921
  - Omit both for an empty body; declaring both fails. Body limit is 1 MiB.
1922
+ - A short HTML answer is `text` plus a declared content type. There is no
1923
+ `respond.html`; use the `page` handler for anything larger than a snippet:
1924
+
1925
+ ```yaml
1926
+ /:
1927
+ respond:
1928
+ text: "<!doctype html><h1>Hello</h1>"
1929
+ response:
1930
+ headers:
1931
+ Content-Type: text/html; charset=utf-8
1932
+ ```
1933
+
1934
+ The body is served verbatim and the default `nosniff` and `no-store` still
1935
+ apply. Under the `oshp` security profile the CSP (`default-src 'self'`) blocks
1936
+ inline `<script>` and `<style>`, so keep the snippet to markup.
1693
1937
  - Status 204/205 cannot declare a nonempty body. HEAD always suppresses the body.
1694
1938
 
1695
1939
  Functions still return their own Response/status/body. YAML header policy does
@@ -1959,10 +2203,8 @@ compiled and the request path is unchanged. Each policy has its own page:
1959
2203
  | `compression` | [compression](https://github.com/jimhoyd-com/urlcode/blob/main/docs/policies/compression.md) | `Accept-Encoding` negotiation; assets precompressed at snapshot time |
1960
2204
  | `cache` | [cache](https://github.com/jimhoyd-com/urlcode/blob/main/docs/policies/cache.md) | Named strategies from RFC 9111/5861/8246/9213 plus an origin memory cache |
1961
2205
 
1962
- The design and the reasoning behind each choice are in the
1963
- [extensions spike](https://github.com/jimhoyd-com/urlcode/blob/main/docs/archive/2026-09-19/SPIKE-EXTENSIONS.md). Operators who need behavior the
1964
- declarative block cannot express pass host [plugins](https://github.com/jimhoyd-com/urlcode/blob/main/docs/PLUGINS.md) in code; plugins
1965
- are never named in YAML.
2206
+ Operators who need behavior the declarative block cannot express pass host
2207
+ [plugins](https://github.com/jimhoyd-com/urlcode/blob/main/docs/PLUGINS.md) in code; plugins are never named in YAML.
1966
2208
 
1967
2209
  ### Declaring policies
1968
2210
 
@@ -2181,6 +2423,7 @@ site:
2181
2423
  policy: [https://example.com/security-policy]
2182
2424
  preferredLanguages: [en, fr]
2183
2425
  llms: llms.txt
2426
+ notFound: public/404.html
2184
2427
  routes:
2185
2428
  /: {page: {file: public/index.html}}
2186
2429
  ```
@@ -2290,9 +2533,30 @@ refused).
2290
2533
  A project-relative text file served as a `page` route with
2291
2534
  `text/plain; charset=utf-8` and the default `no-cache`.
2292
2535
 
2536
+ #### `notFound` → `/404.html`
2537
+
2538
+ A project-relative `.html`/`.htm` file that answers a request matching no
2539
+ route. It becomes a `page` route at `/404.html` (`text/html; charset=utf-8`,
2540
+ `Cache-Control: no-store`); the runtime serves that page with status **404**
2541
+ for an unmatched `GET` or `HEAD`, with the same project security headers and
2542
+ `nosniff` as the built-in 404. Other methods keep the plain-text `Not found`.
2543
+ A route that matches but is disabled, mismatched by `match:` or denied by a
2544
+ policy keeps its own answer; only "no route matched" uses the page.
2545
+
2546
+ `/404.html` is also reachable directly and answers 200 (the same as on static
2547
+ hosting, which is why the path is fixed). It is one generated route in the
2548
+ `routes`/`audit` counts and `explain` reports it as `site.notFound`; the
2549
+ sitemap leaves it out. A route you declare at `/404.html` wins and is served
2550
+ as the not-found page instead. The page answers every unknown URL, so it cannot depend on the path requested.
2551
+
2552
+ `urlcode build --target static` writes it as the object `404.html` (see
2553
+ [static hosting](https://github.com/jimhoyd-com/urlcode/blob/main/docs/STATIC.md)); point the host's error document at that key.
2554
+ Cloudflare refuses it like any `page` route (no asset binding); the Worker
2555
+ has no per-request fallback page, so use the platform's own 404 asset there.
2556
+
2293
2557
  ### Per-target support
2294
2558
 
2295
- | Target | `robots`, `sitemap`, `securityTxt` (`respond`) | `favicon`, `llms` (`page`) |
2559
+ | Target | `robots`, `sitemap`, `securityTxt` (`respond`) | `favicon`, `llms`, `notFound` (`page`) |
2296
2560
  | --- | --- | --- |
2297
2561
  | self-hosted, Vercel, AWS | served | served |
2298
2562
  | Cloudflare | compiled into the artifact (`build --origin` for absolute URLs) | refused at build time like any `page` route: the target has no asset binding; serve them from the platform's static assets |
@@ -2531,6 +2795,10 @@ and management API. That package has since been retired and unpublished, so no
2531
2795
  supported stored-link extension ships today. Core no longer has a native `link`
2532
2796
  handler or a `dynamicLinks` project flag.
2533
2797
 
2798
+ The `store` extension is the data-owning counterpart: it serves declared,
2799
+ bounded collections as a CRUD API from an operator-owned directory. See
2800
+ [data store](https://github.com/jimhoyd-com/urlcode/blob/main/docs/STORE.md).
2801
+
2534
2802
  A project declares versioned configuration and exclusive route mounts:
2535
2803
 
2536
2804
  ```yaml
@@ -2702,29 +2970,18 @@ Node adapter conformance is not a live-provider deployment claim.
2702
2970
 
2703
2971
  ### Project-level lifecycle hooks
2704
2972
 
2705
- `authorize` and `middleware` let core call *into* an extension. They do not
2706
- let a project hand its own code *to* an extension to run at a defined point.
2707
- That gap matters once an extension has meaningful lifecycle events
2708
- registration, deletion, an administrative action, a link resolution, and so
2709
- on. Presentation already has a standard layering mechanism for this: a
2710
- project customizes an extension's *look* through `urlcode-ui`'s
2711
- `copy`/`extra.css`/`templates` without forking it. Behavior needs the same
2712
- standard, or every extension author (and every project depending on one)
2713
- either reinvents it or forks the extension. `urlcode-auth` and
2714
- `urlcode-admin` independently hit this gap (auth/#35, admin/#32); auth's own
2715
- [SPIKE-AUTH.md](https://github.com/jimhoyd-com/urlcode/blob/main/packages/auth/docs/SPIKE-AUTH.md)
2716
- already scoped a shape for `onSignUp`/`beforeRegister`/`onDelete`.
2717
-
2718
- **The pattern.** An extension with lifecycle events an author judges worth
2719
- exposing should let the project name its own function in the extension's own
2720
- `config`, using the same source shape `function`/`middleware` routes already
2721
- use (a string path, or `{source, export, args}` — `schemas/urlcode.schema.json`),
2722
- and add its own `sandbox` boolean next to it (below). The extension's own
2723
- `activate()` reads that config, and its own runtime dispatch — not a new core
2724
- primitive, an ordinary call the extension package makes with the request
2725
- context it already has — invokes the named function at the lifecycle point
2726
- it defines, with a typed input and a typed verdict the extension's own
2727
- schema documents. For example, an auth-style extension might declare:
2973
+ Extensions expose project customization points through the core hook primitive.
2974
+ Each registration publishes `hooks`, a machine-readable list containing the
2975
+ hook name, whether it is a value-transforming `filter` or side-effect `action`,
2976
+ its description and its input/output JSON Schemas. The extension embeds
2977
+ `extensionHooksSchema(contracts)` in its configuration schema and calls
2978
+ `loadExtensionHooks(config.hooks, contracts, context)` during activation.
2979
+ Core then enforces the common source/export shape, project-root confinement,
2980
+ known names, eager module/export validation, input/output schemas and reload
2981
+ cache busting. Hook entry bytes participate in the project revision, so editing
2982
+ a hook invalidates the operator's extension pin.
2983
+
2984
+ Projects select those declared hooks in the extension's own configuration:
2728
2985
 
2729
2986
  ```yaml
2730
2987
  extensions:
@@ -2740,76 +2997,59 @@ extensions:
2740
2997
  ```
2741
2998
 
2742
2999
  with `beforeRegister` called before an account is created, given a typed
2743
- `{email, traits}`-shaped input and returning a typed verdict (`{allow: true}`
3000
+ `{email, profile?}` input and returning a typed verdict (`{allow: true}`
2744
3001
  or `{allow: false, reason}`), and `onSignUp` called after, for side effects
2745
- such as provisioning a workspace. The exact hook names, input/verdict shapes
2746
- and invocation points are the extension's own design `hooks` is not a core
2747
- schema key — but the source/export/sandbox shape, and the fact that this is
2748
- project code the extension calls rather than an operator callback in
2749
- `host.mjs`, should be consistent across extensions so an author who has
2750
- learned one has learned the pattern.
2751
-
2752
- **Trust and execution mode.** Project-level lifecycle hooks are first-party
2753
- project code, the same category as any `function`/`middleware` route, and
2754
- follow the same trusted-by-default rule with no special case
2755
- (docs/SPIKE-DEFAULT-TRUST-MODEL.md, [FUNCTION-SECURITY.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/FUNCTION-SECURITY.md)):
2756
- trusted, in-process execution by default; a project sets `sandbox: true` on
2757
- a given hook to opt that hook into isolated QuickJS/WASM execution, exactly
2758
- the mechanism `function`/`middleware` routes already use and for the same
2759
- reason the hook's own code, not the trustworthiness of whatever triggered
2760
- it, is what calls for isolation (docs/AI-AUTHORING.md's "Deciding when a
2761
- route needs `sandbox: true`"). This was raised as an open question — whether
2762
- a lifecycle hook should always run sandboxed because it makes a
2763
- security-relevant decision and settled the other way: uniformity with the
2764
- rest of the trust model was chosen over hardwiring isolation for lifecycle
2765
- hooks specifically, the same "no special case" call already made between
2766
- `function` and `middleware` trust. A `beforeRegister` hook enforcing "only
2767
- `@acme.com` may register" is the project's own governance rule over its own
2768
- signup flow; it is not more dangerous than any other route the project
2769
- wrote, and does not get a different default.
2770
-
2771
- Core's own trusted/sandboxed dispatch (`TrustedFunctions`/`FunctionPool`,
2772
- `src/runtime.ts`) is wired to route dispatch, not exposed to extensions — but
2773
- each half of a hook's `sandbox: true` opt-in has its own answer:
2774
-
2775
- - **Trusted (the default, no `sandbox: true`).** No core primitive is needed
2776
- or provided: this is ordinary first-party project code, and the
2777
- extension's own `activate()` already has `ExtensionActivation.root` to
2778
- resolve the hook's `source` against and can `import()` it directly, the
2779
- same way any trusted `function`/`middleware` route does. Do that import
2780
- with a per-activation cache-busting query, the way core's own trusted
2781
- route activation does (`src/trusted-functions.ts`): Node's ESM loader
2782
- caches a resolved module forever by URL, so a plain `import()` of the
2783
- unchanged file URL makes a second activation in the same process keep
2784
- serving the hook code that was on disk at the first one
2785
- (jimhoyd-com/urlcode#198). Only the hook's **entry** module is refreshed
2786
- this way — modules the hook itself imports stay on Node's module cache,
2787
- the same limitation the trusted route path has, so a change to a hook's
2788
- own dependency still needs a process restart.
2789
- - **Sandboxed (`sandbox: true`).** `@jimhoyd/urlcode/sandbox` exports
2790
- `SandboxPool`, the same QuickJS/worker-thread engine that backs a
2791
- sandboxed `function`/`middleware` route — the identical module-allowlist
2792
- walk, memory/stack limits, two-layer deadline enforcement, `maxBytes` and
2793
- response-shape validation, with no separate or weaker engine for
2794
- extensions. It takes an explicit list of `{source, export}` entries
2795
- (resolve a hook's `source` string with the re-exported `functionFile()`,
2796
- the same resolution/validation a native route's `source` gets) instead of
2797
- anything route/YAML-shaped, and `execute({entry, chain}, request, context)`
2798
- in place of a `FunctionRoute`. There is no "trusted" mode exported
2799
- alongside it — `SandboxPool` is only ever the isolated path; see
2800
- [FUNCTION-SECURITY.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/FUNCTION-SECURITY.md) and
2801
- [TYPESCRIPT.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/TYPESCRIPT.md) for the full contract.
2802
-
2803
- An extension honoring a hook's `sandbox: true` is expected to actually
2804
- isolate that invocation through `SandboxPool` now that the primitive exists
2805
- (or document plainly that it does not yet, rather than accepting the field
2806
- and silently running it trusted) — say which, in the extension's own docs,
2807
- so an author reading them is not misled about what opt-in exists.
3002
+ such as provisioning a workspace. Hook names and lifecycle timing remain the
3003
+ extension's domain, while their declaration, loading and discovery are shared.
3004
+
3005
+ Hooks are first-party project code and run trusted in-process by default, with
3006
+ full Node access, like trusted `function` and `middleware` routes. Contract v1
3007
+ does not define an arbitrary-value sandbox hook protocol. A hook reference with
3008
+ `sandbox: true` is rejected during activation rather than silently run trusted.
3009
+ Only the entry module is refreshed during reactivation; its imported dependencies
3010
+ remain in Node's module cache until restart.
3011
+
3012
+ The UI extension exposes `transformView`, a synchronous filter called before a
3013
+ named kit template renders. It receives `{template, view}` and returns the view
3014
+ model to render. Use copy, templates, theme and CSS for ordinary presentation
3015
+ changes; use this hook for project-specific computed view data that those
3016
+ declarative layers cannot express. It also exposes `transformPage`, called
3017
+ before the shared layout renders. It receives the editable title, layout,
3018
+ navigation, account menu and flash message and returns those page fields. This
3019
+ lets a product join auth/admin screens to its own shell without replacing their
3020
+ security or workflow behavior. Both filters are synchronous and trusted.
3021
+
3022
+ ### Building an extension
3023
+
3024
+ An extension package should export a registration factory and, when it supports
3025
+ `urlcode init --with`, a side-effect-free `scaffold` function. The registration:
3026
+
3027
+ 1. Declares its logical name, contract version, supported targets, exact project
3028
+ revision pin and strict configuration/policy schemas.
3029
+ 2. Publishes every project hook through `hooks` and reuses
3030
+ `extensionHooksSchema` plus `loadExtensionHooks`; it does not implement its
3031
+ own path resolver or dynamic-import cache.
3032
+ 3. Publishes an `authoring` contract listing its supported project-owned
3033
+ configuration, theme/copy, component/template, stylesheet and hook surfaces,
3034
+ plus focused `fastChecks`. Keep descriptions concrete enough that an agent
3035
+ can choose a supported surface instead of copying package behavior.
3036
+ 4. Activates all configuration, files, services and hooks before serving a
3037
+ request. Invalid or stale configuration fails activation.
3038
+ 5. Returns `handle` for mounts and optionally `authorize`/`middleware` for route
3039
+ policies. It closes resources it owns.
3040
+ 6. Keeps credentials, storage and provider setup in the operator host. Project
3041
+ YAML contains logical configuration and project-relative hook references.
3042
+
3043
+ Consumers install the package, declare its YAML block and mounts/policies, and
3044
+ register it in `host.mjs`. They modify it through declared configuration,
3045
+ presentation layers and hooks. A fork is reserved for changing behavior the
3046
+ extension has not exposed; that is evidence for a new declarative field or hook.
3047
+ See [Composing a site](https://github.com/jimhoyd-com/urlcode/blob/main/docs/COMPOSING-A-SITE.md) for the complete ui/auth/admin example.
2808
3048
 
2809
3049
  ### Discovering schemas
2810
3050
 
2811
3051
  Each registration carries the JSON Schemas that validate its `config` block and
2812
- its per-route policy requirements. `urlcode extensions` prints them together with
3052
+ its per-route policy requirements, plus its hook and authoring contracts. `urlcode extensions` prints them together with
2813
3053
  the project's own declarations so an author can see what a mount accepts:
2814
3054
 
2815
3055
  ```sh
@@ -2818,6 +3058,8 @@ urlcode extensions --project ./site --host-file /absolute/operator/host.mjs [--j
2818
3058
 
2819
3059
  For every registration in the host file it reports the name, contract version,
2820
3060
  targets, credential headers, configuration schema, policy schema (if any),
3061
+ declared hook names, kinds, descriptions and input/output schemas, supported
3062
+ authoring surfaces and their fast checks,
2821
3063
  whether the project declares it, whether its `projectSha256` matches the current
2822
3064
  revision, the routes that mount it and the routes whose policies require it.
2823
3065
  Declared names the host does not register are listed as unregistered. The command
@@ -2890,7 +3132,27 @@ in memory (core zeroes `Uint8Array` contents after writing or on failure). The
2890
3132
  types are exported from `@jimhoyd/urlcode` for packages that want to typecheck
2891
3133
  against them.
2892
3134
 
2893
- Assembly rules, in `--with` order:
3135
+ `--with` is an unordered set. Core sorts the requested names before calling
3136
+ each `scaffold` (so `names` is the same for every spelling), then orders the
3137
+ results from the optional declarative fields on `ScaffoldResult`:
3138
+
3139
+ - `provides`: capability names the extension offers (for example `ui.kit`);
3140
+ a capability must not equal an extension name.
3141
+ - `requires`: extensions or capabilities that must be in the set and are
3142
+ placed before this extension. A missing one refuses, naming both.
3143
+ - `after`: the same ordering, without requiring presence.
3144
+ - `conflicts`: extensions or capabilities that must not be in the set.
3145
+ - Risky-scaffold acknowledgements are one generic channel, not a flag per risk. The operator repeats `--ack <extension>:<id>` (both parts lowercase letters, digits and hyphens, for example `store:public-write`); core validates the syntax, de-duplicates and sorts the values and hands them to every scaffold as the opaque `ScaffoldRequest.acknowledgements` (always present, possibly empty). An extension reads only ids qualified with its own name. To require one it throws an `Error` whose message states the risk and that carries `acknowledgement: '<name>:<id>'`; core appends the exact re-run command (the same `--with`, `--pin` and `--no-manifest`, plus every `--ack` already given and the new one), so an agent meets the acknowledgement only when it reaches that risk and never needs it in advance. A scaffold that used one lists it in `ScaffoldResult.acknowledged`; core refuses any `--ack` that no scaffold listed (a typo, an extension not in `--with`, or an unneeded value), before anything is written, and refuses a scaffold that lists an id it was not given or that is not its own. Acknowledgements are never written to project YAML; they are visible in command history, and the extension should state the resulting model in its generated README. `routeNotes` (single-line strings) are written as comments above that extension's routes; core renders them and infers no policy. Core adds no per-extension flag or result field: a new risk needs only a new id in the extension that owns it. `--allow-public-write` was a store-specific predecessor of this channel; it was never in a published core release and is removed rather than aliased (see [store](https://github.com/jimhoyd-com/urlcode/blob/main/docs/STORE.md)).
3146
+
3147
+ Core topologically orders by these, taking the lexically smallest ready
3148
+ extension first, so every permutation of the same set produces the same host,
3149
+ `urlcode.yaml` activation order and README. A cycle or a missing requirement or
3150
+ conflict refuses before anything is written, naming the extensions involved.
3151
+ Core never adds an extension (auth or ui) and never infers security policy
3152
+ from the set. Host setup should be self-contained (own identifiers, such as
3153
+ `storeProjectSha256`) unless it declares `requires` for what it references.
3154
+
3155
+ Assembly rules, in the resolved order:
2894
3156
 
2895
3157
  - Every package is resolved and every `scaffold` is called before anything is
2896
3158
  written. A name that is not installed refuses with the `npm install` command;
@@ -2904,7 +3166,7 @@ Assembly rules, in `--with` order:
2904
3166
  naming both sources.
2905
3167
  - `host.mjs` is all `hostImports`, then all `hostSetup` lines, then an
2906
3168
  `extensions` array of every `hostEntries` item, then `close()` running the
2907
- `hostClose` statements in reverse `--with` order so later entries release
3169
+ `hostClose` statements in reverse resolved order so later entries release
2908
3170
  before what they built on. Setup lines share one module scope: admin's entry
2909
3171
  references the `service`, `csrfKey` and `projectSha256` identifiers that
2910
3172
  auth's setup defines, which is why `names` carries the full list.
@@ -3007,8 +3269,10 @@ through the kit and refuse to activate without their own templates present.
3007
3269
 
3008
3270
  #### Supported combinations
3009
3271
 
3010
- `--with` order is the activation order, and the kit must be active before
3011
- anything that renders through it.
3272
+ `--with` is an unordered set. Core derives the activation order from each
3273
+ extension's declared requirements, so the kit is active before anything that
3274
+ renders through it, whatever order you name them in. A missing requirement
3275
+ (for example admin without auth) refuses before anything is written.
3012
3276
 
3013
3277
  | `--with` | Result |
3014
3278
  |---|---|
@@ -3018,6 +3282,8 @@ anything that renders through it.
3018
3282
  | `auth` or `auth,admin` | Refused: the scaffold names the missing `ui`. |
3019
3283
  | `auth,admin,ui` | Refused: `ui` must come before the extensions it renders. |
3020
3284
  | `admin` without `auth` | Refused: admin reuses auth's service, CSRF key and revision. |
3285
+ | `ui,auth,store` | Todo API and CRUD screen, both protected by `auth: true`. |
3286
+ | `store` or `ui,store` | Refused: the writable mount would be public. Add `auth`, or re-run the printed command with `--ack store:public-write` for a documented public-write scaffold; core rejects any `--ack` no scaffold consumed, such as one with auth composed or `store` absent. |
3021
3287
 
3022
3288
  Every refusal happens before anything is written, and leaves no directory
3023
3289
  behind. There is no auth-without-ui or admin-without-ui configuration in this
@@ -3088,12 +3354,20 @@ middleware, and the host-file trust boundary.
3088
3354
  The `ui` config's `copy`, `templates` and `stylesheet` paths point at the
3089
3355
  project's own directories. Nothing here forks a package.
3090
3356
 
3357
+ Treat the result as one application. Auth and admin keep ownership of sessions,
3358
+ CSRF, permissions, validation and mutations; the project owns its brand,
3359
+ product navigation and the smallest presentation differences it needs. Inspect
3360
+ `urlcode extensions --host-file ... --json` (MCP: `get_extensions`) and follow
3361
+ each registration's `authoring.surfaces` before copying package code.
3362
+
3091
3363
  | Override | File | Effect |
3092
3364
  |---|---|---|
3093
3365
  | Wording and translation | `ui/copy/<locale>.json` | Replaces catalogue ids, including ids the auth and admin packages own. Listed in `languages`. |
3094
3366
  | A whole screen | `ui/templates/<name>.html` | Shadows a kit or extension template of that name, for example `ui/templates/auth/sign-in.html` or `ui/templates/admin/dashboard.html`. |
3095
3367
  | Styling | `ui/extra.css` | Appended after the kit stylesheet; `{file, replace: true}` replaces it instead. |
3096
3368
  | Colours, logo, favicon, radius, font | the `theme` block | Declarative; no file needed. |
3369
+ | Computed view data | `extensions.ui.config.hooks.transformView` | Adds project data before a named template renders. |
3370
+ | Product shell and navigation | `extensions.ui.config.hooks.transformPage` | Changes title, layout, navigation, account menu or flash before the shared layout renders. |
3097
3371
 
3098
3372
  A template is data in the kit's own language. It cannot add a script, change
3099
3373
  what a form validates, or change what a page sends in headers — so an override
@@ -3139,6 +3413,13 @@ not run it. With the packages named:
3139
3413
  generated README with the flag already set. `@jimhoyd/urlcode-ui` depends on
3140
3414
  neither peer; the operator names them.
3141
3415
 
3416
+ Run the extension's published `fastChecks` while editing. Theme and copy changes
3417
+ need no framework build. Template and CSS checks load only the UI kit and named
3418
+ namespaces; the full runtime validation and request suite remain the final
3419
+ evidence. A React product frontend with `components.json` should also install
3420
+ and follow the official shadcn/ui skill. The server template kit uses compatible
3421
+ tokens but does not accept React components.
3422
+
3142
3423
  Overrides of extension templates and of extension-owned catalogue ids reach
3143
3424
  the rendered screens, which is what the regression test below asserts.
3144
3425
 
@@ -3162,15 +3443,24 @@ extensions:
3162
3443
  onSignUp: ./hooks/on-signup.mjs
3163
3444
  ```
3164
3445
 
3165
- `hooks` is not a core schema key. It is each package's own config, validated
3166
- by that package's schema before `activate()`, which is why `get_extensions` is
3167
- the place to check what a given version accepts.
3446
+ `hooks` remains each package's own config, but core supplies the reference
3447
+ schema, trusted loader and machine-readable hook contract. `get_extensions`
3448
+ reports those contracts, so an agent can discover accepted names, purpose and
3449
+ input/output shapes without guessing from prose.
3168
3450
 
3169
3451
  Hooks are first-party project code and run **trusted and in-process**, the
3170
3452
  same default `function` and `middleware` routes have
3171
- ([FUNCTION-SECURITY.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/FUNCTION-SECURITY.md)). Neither package implements
3172
- sandboxed hook execution yet; `sandbox: true` on a hook is rejected loudly at
3173
- activation rather than accepted and silently run trusted.
3453
+ ([FUNCTION-SECURITY.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/FUNCTION-SECURITY.md)). Extension hook contract v1 is
3454
+ trusted-only; `sandbox: true` is rejected loudly at activation.
3455
+
3456
+ #### `@jimhoyd/urlcode-ui`
3457
+
3458
+ | Hook | Input | Returns | Called |
3459
+ |---|---|---|---|
3460
+ | `transformView` | `{template, view}` | the view object to render | Synchronously before each public `ui.kit.render()` or `ui.kit.page()` call. Use it only when theme, copy, templates and CSS cannot express the change. |
3461
+
3462
+ For example, `transformView: ./hooks/ui-view.mjs` can add project-computed
3463
+ navigation or labels to an auth/admin view without editing either package.
3174
3464
 
3175
3465
  #### `@jimhoyd/urlcode-auth`
3176
3466
 
@@ -3338,14 +3628,11 @@ machinery either way.
3338
3628
 
3339
3629
  This engine — worker spawning, the module-allowlist walk, the two-layer
3340
3630
  deadline, `maxBytes` and response-shape validation — is one implementation
3341
- shared by route dispatch and by `@jimhoyd/urlcode/sandbox`'s `SandboxPool`,
3342
- the public primitive an extension package uses to run a project-supplied hook
3343
- through this exact isolation when the project's own config declares
3344
- `sandbox: true` on it (see [EXTENSIONS.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md#project-level-lifecycle-hooks)
3345
- and [TYPESCRIPT.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/TYPESCRIPT.md)). Every guarantee above applies identically
3346
- through that entry point; there is no separate, weaker sandbox for extensions
3347
- to reach for, and no "trusted" mode exported there — trusted execution needs
3348
- no primitive at all (docs/SPIKE-DEFAULT-TRUST-MODEL.md).
3631
+ shared by route dispatch and by `@jimhoyd/urlcode/sandbox`'s `SandboxPool`.
3632
+ That public HTTP-shaped primitive remains available to extension authors, but
3633
+ project extension hooks use arbitrary typed values and contract v1 runs them
3634
+ trusted in-process; it rejects `sandbox: true` rather than claiming HTTP sandbox
3635
+ semantics apply to them. See [extensions](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md#project-level-lifecycle-hooks).
3349
3636
 
3350
3637
  ### What the trusted default can and can't do
3351
3638