@jimhoyd/urlcode 0.4.0-alpha.2 → 0.4.1

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 (147) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +17 -19
  2. package/.claude/skills/urlcode-operations/SKILL.md +9 -9
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CONTRIBUTING.md +20 -16
  5. package/README.md +59 -64
  6. package/ROADMAP.md +66 -376
  7. package/dist/BUILD-MANIFEST.json +26 -25
  8. package/dist/agents-guide.js +6 -6
  9. package/dist/authoring.js +15 -1
  10. package/dist/build-static.js +2 -0
  11. package/dist/capability-query.js +0 -1
  12. package/dist/catalog.js +0 -1
  13. package/dist/cli.js +25 -9
  14. package/dist/config.js +1 -1
  15. package/dist/explain-cli.js +4 -2
  16. package/dist/explain.js +8 -2
  17. package/dist/extensions.js +1 -1
  18. package/dist/http-response.js +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/init-with.js +36 -11
  21. package/dist/manifest.js +8 -2
  22. package/dist/mcp-authoring.js +4 -4
  23. package/dist/mcp.js +1 -1
  24. package/dist/policies/cache.js +2 -2
  25. package/dist/policies.js +3 -1
  26. package/dist/prerender.js +4 -0
  27. package/dist/project-dependencies.js +305 -0
  28. package/dist/readiness.js +3 -0
  29. package/dist/route-diff.js +12 -5
  30. package/dist/runtime.js +1 -1
  31. package/dist/trusted-functions.js +4 -5
  32. package/dist/types/authoring.d.ts +9 -1
  33. package/dist/types/capability-query.d.ts +0 -1
  34. package/dist/types/catalog.d.ts +0 -4
  35. package/dist/types/config.d.ts +1 -9
  36. package/dist/types/explain.d.ts +6 -1
  37. package/dist/types/extensions.d.ts +1 -1
  38. package/dist/types/http-response.d.ts +0 -1
  39. package/dist/types/index.d.ts +1 -0
  40. package/dist/types/init-with.d.ts +7 -13
  41. package/dist/types/manifest.d.ts +5 -2
  42. package/dist/types/project-dependencies.d.ts +78 -0
  43. package/dist/types/readiness.d.ts +2 -0
  44. package/dist/types/trusted-functions.d.ts +1 -4
  45. package/dist/types/types.d.ts +8 -1
  46. package/dist/types.js +8 -1
  47. package/dist/typescript-authoring.js +18 -7
  48. package/docs/AI-AUTHORING.md +15 -7
  49. package/docs/ASSETS.md +2 -1
  50. package/docs/AUTH-BACKUP.md +32 -0
  51. package/docs/AWS.md +9 -0
  52. package/docs/BEST-PRACTICES.md +17 -9
  53. package/docs/CAPABILITIES.md +1 -1
  54. package/docs/CI-FOLLOWUP-2026-09-19.md +97 -0
  55. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +322 -0
  56. package/docs/CI.md +8 -3
  57. package/docs/CODEBASE-AUDIT-2026-09-20.md +284 -0
  58. package/docs/COMPOSING-A-SITE.md +278 -0
  59. package/docs/DEVELOPMENT-PIPELINE.md +270 -0
  60. package/docs/EXTENSIONS.md +51 -11
  61. package/docs/FRAMEWORK.md +67 -48
  62. package/docs/FUNCTION-SECURITY.md +44 -0
  63. package/docs/INSTALL.md +13 -8
  64. package/docs/LOCAL-DEVELOPMENT.md +3 -0
  65. package/docs/MIDDLEWARE.md +10 -4
  66. package/docs/OPEN-DECISIONS.md +224 -212
  67. package/docs/OPERATIONAL-PROOF.md +3 -3
  68. package/docs/OPERATIONS.md +3 -3
  69. package/docs/POLICIES.md +13 -5
  70. package/docs/PRERENDER.md +23 -11
  71. package/docs/PROJECT-DIRECTION.md +3 -3
  72. package/docs/READINESS.md +10 -5
  73. package/docs/README.md +20 -44
  74. package/docs/RELEASE-0.4.0-alpha.3.md +50 -0
  75. package/docs/RELEASE-0.4.1.md +73 -0
  76. package/docs/RELEASE-READINESS.md +6 -6
  77. package/docs/RELEASE-SECURITY.md +96 -181
  78. package/docs/RESILIENCE.md +3 -3
  79. package/docs/ROUTING.md +3 -4
  80. package/docs/SECURITY-AUDIT.md +2 -2
  81. package/docs/SPECIFICATION.md +12 -6
  82. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +6 -5
  83. package/docs/SPIKE-BUSINESS-SUITE.md +14 -6
  84. package/docs/SPIKE-CORE-LAYERING.md +49 -18
  85. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +7 -5
  86. package/docs/STARTERS.md +17 -5
  87. package/docs/STATIC.md +14 -3
  88. package/docs/TOOLING.md +10 -7
  89. package/docs/TYPESCRIPT-AUTHORING.md +6 -1
  90. package/docs/VERCEL.md +10 -2
  91. package/docs/VERSION-ALIGNMENT.md +76 -201
  92. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +68 -0
  93. package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +102 -0
  94. package/docs/{NEXT-PHASE-PLAN.md → archive/2026-09-19/NEXT-PHASE-PLAN.md} +15 -5
  95. package/docs/{NEXT-STEPS.md → archive/2026-09-19/NEXT-STEPS.md} +15 -3
  96. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +277 -0
  97. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +186 -0
  98. package/docs/archive/2026-09-19/ROADMAP.md +387 -0
  99. package/docs/{SPIKE-EXTENSION-MODEL.md → archive/2026-09-19/SPIKE-EXTENSION-MODEL.md} +11 -0
  100. package/docs/{SPIKE-EXTENSIONS.md → archive/2026-09-19/SPIKE-EXTENSIONS.md} +25 -14
  101. package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +365 -0
  102. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +778 -0
  103. package/docs/{USABILITY-REVIEW.md → archive/2026-09-19/USABILITY-REVIEW.md} +12 -2
  104. package/docs/archive/README.md +28 -0
  105. package/docs/policies/agents.md +1 -1
  106. package/docs/policies/compression.md +3 -2
  107. package/docs/policies/security.md +3 -2
  108. package/docs/yaml/functions.md +10 -2
  109. package/docs/yaml/middleware.md +5 -3
  110. package/examples/assets/example.yaml +1 -1
  111. package/examples/cookbook/middleware/envelope.mjs +4 -2
  112. package/examples/cookbook/route-index.json +1 -1
  113. package/examples/cookbook/routes/middleware.yaml +1 -1
  114. package/examples/prerender/README.md +14 -6
  115. package/examples/prerender/functions/page.mjs +4 -2
  116. package/examples/prerender/middleware/template.mjs +1 -1
  117. package/examples/prerender/prerender.mjs +1 -1
  118. package/examples/prerender/urlcode.yaml +8 -4
  119. package/llms-full.txt +503 -88
  120. package/llms.txt +6 -4
  121. package/package.json +27 -4
  122. package/packaging/claude-plugin/.claude-plugin/plugin.json +2 -2
  123. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +17 -19
  124. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +9 -9
  125. package/recipes/authenticated-json-api/README.md +4 -3
  126. package/recipes/authenticated-json-api/functions/profile.mjs +2 -1
  127. package/recipes/authenticated-json-api/recipe.yaml +1 -1
  128. package/recipes/contact-form/functions/contact.mjs +2 -1
  129. package/recipes/contact-form/recipe.yaml +2 -2
  130. package/recipes/cors-api/README.md +2 -2
  131. package/recipes/cors-api/recipe.yaml +1 -1
  132. package/recipes/health-page/README.md +1 -1
  133. package/recipes/json-api/README.md +1 -1
  134. package/recipes/json-api/recipe.yaml +3 -3
  135. package/recipes/middleware/README.md +8 -4
  136. package/recipes/middleware/middleware/envelope.mjs +4 -2
  137. package/recipes/protected-download/README.md +1 -1
  138. package/recipes/protected-download/recipe.yaml +1 -1
  139. package/recipes/static-plus-api/README.md +2 -2
  140. package/recipes/static-plus-api/public/index.html +1 -1
  141. package/recipes/static-plus-api/recipe.yaml +1 -1
  142. package/recipes/static-plus-api/urlcode.yaml +1 -1
  143. package/recipes/typescript/recipe.yaml +4 -4
  144. package/skills/urlcode/SKILL.md +6 -6
  145. package/starters/default/AGENTS.md +6 -6
  146. package/docs/SPIKE-LAMBDA-COMPILE.md +0 -201
  147. package/docs/SPIKE-MONOREPO.md +0 -322
package/llms-full.txt CHANGED
@@ -1,5 +1,5 @@
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: 15 documents, about 47,012 tokens (estimate: characters / 4). -->
2
+ <!-- Consolidated URLCode authoring reference: 16 documents, about 52,663 tokens (estimate: characters / 4). -->
3
3
 
4
4
  # URLCode
5
5
 
@@ -36,7 +36,8 @@ Agents that explicitly want the complete consolidated reference in one fetch sho
36
36
  12. [Conditional routing](#conditional-routing)
37
37
  13. [Bounded proxy and webhook transport](#bounded-proxy-and-webhook-transport)
38
38
  14. [Operator-installed extensions](#operator-installed-extensions)
39
- 15. [Function execution: trusted by default, sandboxed opt-in](#function-execution-trusted-by-default-sandboxed-opt-in)
39
+ 15. [Composing a site from ui, auth and admin](#composing-a-site-from-ui-auth-and-admin)
40
+ 16. [Function execution: trusted by default, sandboxed opt-in](#function-execution-trusted-by-default-sandboxed-opt-in)
40
41
 
41
42
  ---
42
43
 
@@ -51,26 +52,26 @@ claim here is implemented in the linked repository; nothing is roadmap.
51
52
 
52
53
  ### Four packages, one project shape
53
54
 
54
- | Package | Repository | What it adds | How a project declares it |
55
+ | Package | Source | What it adds | How a project declares it |
55
56
  |---|---|---|---|
56
- | `@jimhoyd/urlcode` | [urlcode](https://github.com/jimhoyd-com/urlcode) | The runtime: YAML routes, functions and middleware (trusted by default, `sandbox: true` opt-in), pages and assets, policies, site conventions, CLI, provider adapters, the extension contract | `urlcode.yaml` with `version: "1"` |
57
- | `@jimhoyd/urlcode-ui` | [urlcode-ui](https://github.com/jimhoyd-com/urlcode-ui) | Shared presentation: escaped templates, shadcn/ui partials, one stylesheet with light and dark, themes, translations, the `ui` extension that serves the kit's assets | `extensions.ui` plus an asset mount route |
58
- | `@jimhoyd/urlcode-auth` | [urlcode-auth](https://github.com/jimhoyd-com/urlcode-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 |
59
- | `@jimhoyd/urlcode-admin` | [urlcode-admin](https://github.com/jimhoyd-com/urlcode-admin) | Administration: users, sessions, roles, audit, registration approval, two-person cases, support impersonation, health | `extensions.admin` plus an `/admin/*` mount |
60
-
61
- The core is Apache-2.0 and released. The three extension packages are
62
- Apache-2.0 and published to npm as alphas (`@jimhoyd/urlcode-ui@0.1.0-alpha.4`,
63
- `@jimhoyd/urlcode-auth@0.1.0-alpha.2`, `@jimhoyd/urlcode-admin@0.1.0-alpha.2`,
64
- on core `0.4.0-alpha.1`). An alpha on npm is a distribution channel, not an
65
- endorsement: the source is complete, but independent review, deployment
66
- evidence and an accessibility assessment are still pending
57
+ | `@jimhoyd/urlcode` | this repository | The runtime: YAML routes, functions and middleware (trusted by default, `sandbox: true` opt-in), pages and assets, policies, site conventions, CLI, provider adapters, the extension contract | `urlcode.yaml` with `version: "1"` |
58
+ | `@jimhoyd/urlcode-ui` | [`packages/ui`](https://github.com/jimhoyd-com/urlcode/blob/main/packages/ui) | Shared presentation: escaped templates, shadcn/ui partials, one stylesheet with light and dark, themes, translations, the `ui` extension that serves the kit's assets | `extensions.ui` plus an asset mount route |
59
+ | `@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
+ | `@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
+ 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
67
66
  ([issue 58](https://github.com/jimhoyd-com/urlcode/issues/58)). Their status
68
- files say exactly what is built: [auth](https://github.com/jimhoyd-com/urlcode-auth/blob/main/IMPLEMENTATION-STATUS.md),
69
- [admin](https://github.com/jimhoyd-com/urlcode-admin/blob/main/IMPLEMENTATION-STATUS.md),
70
- [ui](https://github.com/jimhoyd-com/urlcode-ui/blob/main/IMPLEMENTATION-STATUS.md).
71
- Which core version each package supports, how it declares that, and the order
72
- in which a core change reaches the downstream repositories are recorded in
73
- [core version alignment](https://github.com/jimhoyd-com/urlcode/blob/main/docs/VERSION-ALIGNMENT.md).
67
+ files say exactly what is built: [auth](https://github.com/jimhoyd-com/urlcode/blob/main/packages/auth/IMPLEMENTATION-STATUS.md),
68
+ [admin](https://github.com/jimhoyd-com/urlcode/blob/main/packages/admin/IMPLEMENTATION-STATUS.md),
69
+ [ui](https://github.com/jimhoyd-com/urlcode/blob/main/packages/ui/IMPLEMENTATION-STATUS.md).
70
+ The current version of each package is its own manifest, and the peer ranges it
71
+ declares are in that manifest too; do not read a version number out of this
72
+ page. How versions, channels and release tags line up is recorded in
73
+ [package and channel alignment](https://github.com/jimhoyd-com/urlcode/blob/main/docs/VERSION-ALIGNMENT.md), and `npm run
74
+ release:status` reports the live registry and tag state.
74
75
 
75
76
  ### The ladder
76
77
 
@@ -87,8 +88,10 @@ Each rung's YAML is valid on every rung above it.
87
88
  3. **Functions and middleware.** `function` routes and ordered `middleware`
88
89
  in JavaScript, trusted and in-process by default; a route declaring
89
90
  `sandbox: true` runs isolated instead (QuickJS inside WebAssembly, fresh
90
- heap per call, no Node, filesystem or network). Secrets reach a function
91
- only through an operator grant pinned to the project revision.
91
+ heap per call, no Node, filesystem or network). The `env`/`secrets` the
92
+ runtime injects into a function come only from an operator grant pinned to
93
+ the project revision; the grant governs that injected context, not the
94
+ ambient Node environment trusted in-process code can reach on its own.
92
95
  4. **Accounts.** The `auth` extension: sign-in, registration, MFA, account
93
96
  page and protected routes. The operator installs it in a host file outside
94
97
  the project; YAML only declares the mount and configuration.
@@ -100,27 +103,29 @@ Each rung's YAML is valid on every rung above it.
100
103
  project copy, template and stylesheet overrides for kit-rendered pages.
101
104
 
102
105
  Stored short links previously sat here as a native `link` route; that handler
103
- was removed from core. A future `urlcode-dynamic-link` package (not yet
104
- published, so this one line is the exception to "nothing is roadmap" above)
105
- is expected to own them the same way `auth`/`admin` own their mounts.
106
+ was removed from core. A `urlcode-dynamic-link` package owned them the same way
107
+ `auth`/`admin` own their mounts, but it has been retired and unpublished; no
108
+ package occupies this rung today.
106
109
 
107
110
  Rungs 1 to 3 need only the core package. Rungs 4 to 6 need the extension
108
- packages, installed from npm as `0.1.0-alpha.x` prereleases, and a Node host
109
- with a patched SQLite build; see each repository's README for the exact
110
- requirement.
111
+ packages installed from npm and a Node host with a
112
+ patched SQLite build; see each package's README ([auth](https://github.com/jimhoyd-com/urlcode/blob/main/packages/auth/README.md),
113
+ [admin](https://github.com/jimhoyd-com/urlcode/blob/main/packages/admin/README.md), [ui](https://github.com/jimhoyd-com/urlcode/blob/main/packages/ui/README.md)) for the
114
+ exact requirement.
111
115
 
112
116
  ### The composition contract
113
117
 
114
- An extended project starts with the packages and one command:
118
+ After the aligned `0.4.1` packages are published, an extended project starts
119
+ with the packages and one command:
115
120
 
116
121
  ```sh
117
- npm install @jimhoyd/urlcode @jimhoyd/urlcode-ui @jimhoyd/urlcode-auth @jimhoyd/urlcode-admin
118
- urlcode init my-site --with auth,admin
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
123
+ urlcode init my-site --with ui,auth,admin
119
124
  ```
120
125
 
121
- Installing from npm is the normal path; the extension repositories'
122
- `pack-sources.mjs` still builds local tarballs from a reviewed checkout for
123
- operators who install only source they have read. Three files make an extended
126
+ Installing from npm is the normal path; `scripts/pack-sources.mjs` still builds
127
+ local tarballs from a reviewed checkout for operators who install only source
128
+ they have read one revision now covers core and every extension. Three files make an extended
124
129
  project. Nothing else is discovered by convention.
125
130
 
126
131
  ```
@@ -152,7 +157,8 @@ routes:
152
157
  auth: {}
153
158
  ```
154
159
 
155
- The operator host wires the packages, once, in code the project cannot reach:
160
+ The operator host explicitly registers the packages. Registration is an
161
+ activation boundary; it does not isolate trusted application code from the host:
156
162
 
157
163
  ```js
158
164
  import { createUiExtension } from '@jimhoyd/urlcode-ui/host';
@@ -172,18 +178,18 @@ export default {
172
178
  };
173
179
  ```
174
180
 
175
- Auth and admin pages currently render through the shared primitives and a
176
- `presentation` (catalogue and `--ui-*` theme variables). The `ui` extension
177
- serves the template kit and its assets for pages an extension renders through
178
- `ui.kit`; moving the auth and admin screens onto the kit is a listed remaining
179
- item in the ui repository, so today one `presentation` restyles them and the
180
- `ui` block is optional.
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.
181
187
 
182
188
  ```sh
183
189
  urlcode serve --project /absolute/site --host-file /absolute/operator/host.mjs --origin https://site.example
184
190
  ```
185
191
 
186
- `urlcode init <dir> --with auth,admin` writes this layout in one step: it
192
+ `urlcode init <dir> --with ui,auth,admin` writes this layout in one step: it
187
193
  resolves each installed `@jimhoyd/urlcode-<name>` from the current directory,
188
194
  calls its `scaffold` export and merges the fragments into `app/urlcode.yaml`,
189
195
  one `host.mjs` and one `README.md`, refusing before writing anything when a
@@ -194,12 +200,24 @@ administrator from JSON on stdin. `inspectExtensionRevision(project)` prints
194
200
  the SHA-256 that `projectSha256` must carry; changing extension YAML, policies
195
201
  or mounts changes the revision and needs an explicit operator reapproval.
196
202
 
203
+ The presentation tooling composes the same way, by naming packages rather than
204
+ depending on them. `urlcode-ui` is the kit alone until
205
+ `--extensions @jimhoyd/urlcode-auth,@jimhoyd/urlcode-admin` names the packages
206
+ that ship the other namespaces: each is resolved from `--project` with Node
207
+ package resolution and imported for the namespace it exports, so `list`,
208
+ `doctor`, `eject`, `preview` and `copy --missing` cover the `auth/*` and
209
+ `admin/*` templates and copy the host registers, and a project override of an
210
+ extension template is checked against the shipped view model. A package that is
211
+ not installed is skipped. `urlcode init --with` writes the commands with the
212
+ flag already set; `@jimhoyd/urlcode-ui` still depends on neither peer.
213
+
197
214
  ### Rules an agent must follow
198
215
 
199
216
  These are the facts that keep generated projects valid. The full matrix is in
200
217
  [AI authoring](https://github.com/jimhoyd-com/urlcode/blob/main/docs/AI-AUTHORING.md); this is the short list.
201
218
 
202
- - **YAML never names code, packages, databases or credentials.** Extensions are
219
+ - **Extension YAML names logical extensions, not host packages or credentials.**
220
+ Function and middleware `source` fields do name project modules. Extensions are
203
221
  logical names; the host file chooses the implementation. There is no
204
222
  `--extension` flag, no `import` in YAML, no interpolation.
205
223
  - **One handler per route.** `redirect`, `respond`, `page`, `static`, `download`,
@@ -210,13 +228,15 @@ These are the facts that keep generated projects valid. The full matrix is in
210
228
  It runs in-process with full Node access unless the route declares
211
229
  `sandbox: true`, which isolates it to a text/JSON `Request`/`Response`
212
230
  subset, validated `args` and granted `env`, with no `fetch`, Node,
213
- filesystem or timers. Either way, `args`/`env`/`secrets` are exactly what
231
+ filesystem or general network access; bounded timers are available. Either way, `args`/`env`/`secrets` are exactly what
214
232
  the route declares and an operator grants — trust changes where code runs,
215
233
  not what it is handed. See docs/SPIKE-DEFAULT-TRUST-MODEL.md and
216
234
  docs/FUNCTION-SECURITY.md.
217
235
  - **Authentication is host processing.** Do not build login forms, session
218
- cookies or password checks in functions. Declare `policies.extensions.auth`
219
- on the route; the runtime withholds `Cookie` and `Authorization` from guests.
236
+ cookies or password checks in functions. With the auth extension declared,
237
+ prefer `auth: true` or `auth: {role: admin}`; these expand to
238
+ `policies.extensions.auth`. The runtime filters credential headers passed to
239
+ application handlers. This is not a security boundary against trusted Node code.
220
240
  - **Everything is validated before it runs.** `urlcode validate --local`,
221
241
  `urlcode test`, `urlcode audit --expect-routes N`. Unsupported features fail
222
242
  with the route named; nothing degrades silently.
@@ -235,10 +255,10 @@ These are the facts that keep generated projects valid. The full matrix is in
235
255
  | Need | Read |
236
256
  |---|---|
237
257
  | Write or change routes | [YAML guide](https://github.com/jimhoyd-com/urlcode/blob/main/docs/YAML-GUIDE.md), [field reference](https://github.com/jimhoyd-com/urlcode/blob/main/docs/YAML-REFERENCE.md), [cookbook](https://github.com/jimhoyd-com/urlcode/blob/main/examples/cookbook/README.md) |
238
- | Add accounts | [auth README](https://github.com/jimhoyd-com/urlcode-auth#readme), [auth security](https://github.com/jimhoyd-com/urlcode-auth/blob/main/SECURITY.md) |
239
- | Add administration | [admin README](https://github.com/jimhoyd-com/urlcode-admin#readme) |
240
- | Restyle every page | [ui README](https://github.com/jimhoyd-com/urlcode-ui#readme), [ui contract](https://github.com/jimhoyd-com/urlcode-ui/blob/main/CONTRACT.md) |
241
- | 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/SPIKE-EXTENSION-MODEL.md) |
258
+ | 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
+ | Add administration | [admin README](https://github.com/jimhoyd-com/urlcode/blob/main/packages/admin/README.md) |
260
+ | 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) |
242
262
  | 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) |
243
263
 
244
264
  ---
@@ -272,7 +292,11 @@ fields or bypass target limits or operator grants. See [the design principle](ht
272
292
  6. [Readiness](https://github.com/jimhoyd-com/urlcode/blob/main/docs/READINESS.md), [capacity](https://github.com/jimhoyd-com/urlcode/blob/main/docs/CAPACITY.md), [DDoS/recovery](https://github.com/jimhoyd-com/urlcode/blob/main/docs/RESILIENCE.md).
273
293
  7. [The framework](https://github.com/jimhoyd-com/urlcode/blob/main/docs/FRAMEWORK.md) for accounts, administration and presentation:
274
294
  `extensions.<name>` blocks and `extension` mounts are the only YAML those
275
- packages need; their configuration is documented in their own repositories.
295
+ packages need. [Composing a site](https://github.com/jimhoyd-com/urlcode/blob/main/docs/COMPOSING-A-SITE.md) is the map of what a
296
+ consumer may then change: the `config` each package accepts, the
297
+ presentation overrides under `ui/`, the project functions its lifecycle
298
+ hooks call, and when a requirement instead needs a new extension in
299
+ TypeScript.
276
300
 
277
301
  The root [llms.txt](https://github.com/jimhoyd-com/urlcode/blob/main/llms.txt) is a compact discovery index; the generated
278
302
  [llms-full.txt](https://github.com/jimhoyd-com/urlcode/blob/main/llms-full.txt) concatenates the authoring documents above in
@@ -353,7 +377,7 @@ The benchmark operates locally; it is not a load test of an external deployment.
353
377
  | Parameter validation and JSON body syntax checks | Full OpenAPI or JSON Schema validation of request bodies |
354
378
  | Local test/audit/benchmark | Route-local YAML tests, managed monitoring, production load certification |
355
379
  | Local/self-hosted runtime; limited AWS/Vercel/Cloudflare implementations with local tests | Verified provider deployments or full cross-provider parity |
356
- | File authoring and snapshot reload | General guest storage broker; stored short links (moving to a future `urlcode-dynamic-link` extension package, not yet published) |
380
+ | File authoring and snapshot reload | General guest storage broker; stored short links (no supported package; the `urlcode-dynamic-link` extension was retired) |
357
381
  | Optional host `policies` (`throttle`, `agents`, `security`, `compression`, `cache`) and reusable `profiles` | Plugins named in YAML, shared multi-instance counters, CORS, verified-bot checks |
358
382
  | Optional top-level `site` (`robots`, `sitemap`, `favicon`, `securityTxt`, `llms`) generating native routes | Per-route `noindex` field, sitemap index files, `humans.txt`, signed `security.txt` |
359
383
 
@@ -487,9 +511,12 @@ Put that justification where tooling can see it, not only in a source
487
511
  comment: an optional `sandboxReason` string on the route (up to 500
488
512
  characters, `schemas/urlcode.schema.json`) records why a route needs
489
513
  isolation, or why it is safe to trust, regardless of whether `sandbox` is
490
- `true` or `false`. `urlcode explain`/`context` surface it next to the
491
- route's `sandbox` boolean, so the trust decision has a reviewable trail
492
- without reading every route's source file:
514
+ `true` or `false`. `urlcode explain`/`context`, the manifest and the
515
+ `routes` inventory all surface it next to the route's `sandbox` boolean —
516
+ per route, not per handler, so a native handler that runs `middleware`
517
+ reports its execution mode too, and `routes --compare` shows a flip between
518
+ trusted and sandboxed execution as a changed route. The trust decision has a
519
+ reviewable trail without reading every route's source file:
493
520
 
494
521
  ```yaml
495
522
  routes:
@@ -570,8 +597,9 @@ trust (adding `sandbox: true` without saying why, or relying on the trusted
570
597
  default for code that plainly needed isolation) to work around them.
571
598
 
572
599
  There is no native `link` handler or `dynamicLinks` project flag; both were
573
- removed. Report a request for live stored links as a gap pending the future
574
- `urlcode-dynamic-link` extension package rather than inventing a `link` field.
600
+ removed. The `urlcode-dynamic-link` extension package that briefly owned them
601
+ has been retired and unpublished, so there is no supported replacement. Report a
602
+ request for live stored links as a gap rather than inventing a `link` field.
575
603
 
576
604
  See [capabilities and normalized route representation](https://github.com/jimhoyd-com/urlcode/blob/main/docs/CAPABILITIES.md) for the target catalog,
577
605
  programmatic compatibility analysis and provider verification limits.
@@ -1255,10 +1283,10 @@ keys preserve repeated values. Headers are forwarded only through explicit maps.
1255
1283
  ### Stored links
1256
1284
 
1257
1285
  Core has no native `link` handler or `dynamicLinks` project flag; both were
1258
- removed. Stored short links are moving to a future `urlcode-dynamic-link`
1259
- extension package (mount-based, like `auth`/`admin`), not yet published. A
1260
- project needing them declares an `extension` mount once that package exists;
1261
- there is no in-core replacement or deprecation shim for `link`/`dynamicLinks`.
1286
+ removed. The `urlcode-dynamic-link` extension package that replaced them has
1287
+ been retired and unpublished, so a project needing stored short links owns that
1288
+ storage itself; there is no in-core replacement or deprecation shim for
1289
+ `link`/`dynamicLinks`.
1262
1290
 
1263
1291
  ### Functions
1264
1292
 
@@ -1299,7 +1327,11 @@ entry may likewise be a string, normalized to `{source: <string>}`. Only the
1299
1327
  long form exists after loading, so `routes`, `audit`, `explain`, revision hashes
1300
1328
  and the field reference describe the expansion.
1301
1329
 
1302
- ES modules only (`.mjs` or `.js`, independent of Node package settings).
1330
+ ES modules only. `.mjs` is loaded as ESM in both modes. A `sandbox: true` route
1331
+ also reads `.js` as ESM independently of Node package settings, but a trusted
1332
+ route imports `.js` through Node's own loader, so an ESM `.js` module needs the
1333
+ nearest `package.json` to declare `"type": "module"`; under `"type": "commonjs"`
1334
+ it fails activation. Prefer `.mjs` for modules that must work in either mode.
1303
1335
  [Build-time TypeScript authoring](https://github.com/jimhoyd-com/urlcode/blob/main/docs/TYPESCRIPT-AUTHORING.md) can produce these
1304
1336
  JavaScript modules in a separate output project; serving does not transpile them.
1305
1337
  The build never imports application code into Node, uses fixed compiler settings,
@@ -1426,7 +1458,9 @@ are local Git-owned examples; they grant no capabilities. [Build-time TypeScript
1426
1458
  is separate from runtime execution.
1427
1459
 
1428
1460
  The [tooling SDK and optional local MCP](https://github.com/jimhoyd-com/urlcode/blob/main/docs/TOOLING.md) inspect and validate without
1429
- executing handlers or reading binding values. [Provider conformance](https://github.com/jimhoyd-com/urlcode/blob/main/docs/PROVIDER-VERIFICATION.md)
1461
+ executing handlers or reading binding values. MCP defaults to inspection; the
1462
+ explicit `--allow-authoring` option enables bounded project writes, separately
1463
+ from host-file loading and grants. [Provider conformance](https://github.com/jimhoyd-com/urlcode/blob/main/docs/PROVIDER-VERIFICATION.md)
1430
1464
  distinguishes local adapter replay from actual deployment observations; no real
1431
1465
  provider deployment is implied by CI. [Proxy and signal egress](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EGRESS.md) requires
1432
1466
  external revision-pinned operator grants and bounded host-owned transport;
@@ -1539,10 +1573,9 @@ any per-visitor session record. Git owns route behavior and code; user-created r
1539
1573
  their own persistence, backup and export lifecycle.
1540
1574
 
1541
1575
  Core has no native handler for this today: the `link` handler that implemented
1542
- it was removed. Stored short links are moving to a future
1543
- `urlcode-dynamic-link` extension package (mount-based, like `auth`/`admin`,
1544
- not yet published); a project needing them declares an `extension` mount once
1545
- that package exists.
1576
+ it was removed. The `urlcode-dynamic-link` extension package that replaced it
1577
+ has been retired and unpublished, so a project needing stored short links owns
1578
+ that storage itself.
1546
1579
 
1547
1580
  Functions still cannot access databases, the filesystem or network directly.
1548
1581
  General application state and realtime sessions remain future work.
@@ -1735,12 +1768,18 @@ Every middleware and the function share the same request and context. Validated
1735
1768
  with a fresh `context.state` object for this chain. State never survives the
1736
1769
  request. Header edits are visible downstream; editing the request does not
1737
1770
  reroute it or change already validated inputs. Request bodies are single-use:
1738
- reading one consumes it for downstream code. There is no `clone()` or streaming
1739
- API; pass parsed data through `context.state` when needed.
1771
+ reading one consumes it for downstream code. On a `sandbox: true` route there is
1772
+ no `clone()` or streaming API at all, so pass parsed data through
1773
+ `context.state`; a trusted route receives Node's own `Request`/`Response` and so
1774
+ does have `clone()`, but passing parsed data through `context.state` keeps the
1775
+ chain portable between the two modes.
1740
1776
 
1741
1777
  Function responses support the existing text/JSON guest API. To transform their
1742
- body, read it and return a new `Response`. Native redirect/respond/page/static/
1743
- download bodies are opaque and cannot be read through `text()` or `json()`.
1778
+ body, read it and return a new `Response`. On a `sandbox: true` route, native
1779
+ redirect/respond/page/static/download bodies are opaque and cannot be read
1780
+ through `text()` or `json()`; a trusted chain receives them as an ordinary
1781
+ `Response` and can read them, so wrapping `respond: {text: hello}` and returning
1782
+ `HELLO` works there and fails in the guest.
1744
1783
  Returning the same native response preserves original bytes, including binary
1745
1784
  files, ranges and HEAD lengths. You may add headers, but cannot change its
1746
1785
  original status or existing native headers while preserving that body. To replace
@@ -1790,7 +1829,8 @@ Source: https://github.com/jimhoyd-com/urlcode/blob/main/docs/ASSETS.md
1790
1829
 
1791
1830
  Native file handlers are implemented in 0.1.0-alpha.3. They work in the local
1792
1831
  Node runtime and self-hosted process/container. Provider adapters remain planned.
1793
- They do not run user functions or expose filesystem APIs to sandboxed code.
1832
+ They do not run user functions or expose filesystem APIs to guest code, trusted
1833
+ or sandboxed.
1794
1834
 
1795
1835
  ```yaml
1796
1836
  version: "1"
@@ -1902,9 +1942,10 @@ Source: https://github.com/jimhoyd-com/urlcode/blob/main/docs/POLICIES.md
1902
1942
  Policies are host-enforced behavior declared in YAML around a route: a
1903
1943
  per-client request budget, a User-Agent deny list, a security-header profile,
1904
1944
  content-coding negotiation and an HTTP caching strategy. They run in the host
1905
- process, outside the sandbox, so they can see the client address, keep counters
1906
- across requests and touch the transport, which route middleware by design
1907
- cannot ([middleware](https://github.com/jimhoyd-com/urlcode/blob/main/docs/MIDDLEWARE.md), [function security](https://github.com/jimhoyd-com/urlcode/blob/main/docs/FUNCTION-SECURITY.md)).
1945
+ process, outside function/middleware execution trusted or sandboxed alike
1946
+ so they can see the client address, keep counters across requests and touch
1947
+ the transport, which route middleware by design cannot
1948
+ ([middleware](https://github.com/jimhoyd-com/urlcode/blob/main/docs/MIDDLEWARE.md), [function security](https://github.com/jimhoyd-com/urlcode/blob/main/docs/FUNCTION-SECURITY.md)).
1908
1949
 
1909
1950
  Everything here is optional and off by default. A project with no `policies`
1910
1951
  key and no `profiles` key behaves exactly as before: no policy module is
@@ -1919,7 +1960,7 @@ compiled and the request path is unchanged. Each policy has its own page:
1919
1960
  | `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 |
1920
1961
 
1921
1962
  The design and the reasoning behind each choice are in the
1922
- [extensions spike](https://github.com/jimhoyd-com/urlcode/blob/main/docs/SPIKE-EXTENSIONS.md). Operators who need behavior 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
1923
1964
  declarative block cannot express pass host [plugins](https://github.com/jimhoyd-com/urlcode/blob/main/docs/PLUGINS.md) in code; plugins
1924
1965
  are never named in YAML.
1925
1966
 
@@ -1941,7 +1982,14 @@ routes:
1941
1982
  `policies` at the top level sets project defaults; `routes.<path>.policies`
1942
1983
  adjusts them for one route. Two route-level short forms exist. `auth`
1943
1984
  expands to `policies.extensions.auth` when the project declares an auth
1944
- [extension](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md). `cache: {strategy, maxAge, ...}` expands to
1985
+ [extension](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md), carrying the same keys minus `required`;
1986
+ `required: false` documents the intent and emits no policy. It accepts
1987
+ `required`, `role`, `permission`, `verified`, `freshWithinSeconds` and
1988
+ `onDeny` and nothing else — `role` is singular, and there is no `roles`. Like
1989
+ `cache` below, it is refused rather than silently ignored in three cases: when
1990
+ the project declares no `extensions.auth`, when the route also sets
1991
+ `policies.extensions.auth` (use one form), and when the route sets
1992
+ `policies.extensions: false`. `cache: {strategy, maxAge, ...}` expands to
1945
1993
  `policies.cache` the same way — the compiler merges it into that route's
1946
1994
  `policies` before anything else reads the project, so `routes`, `audit` and
1947
1995
  `explain` see only the expanded long form, and it is refused alongside a
@@ -2477,10 +2525,11 @@ and admin implementations live in `urlcode-auth` and `urlcode-admin`; the runtim
2477
2525
  supplies only the generic integration contract. No project file can import a host
2478
2526
  extension or choose its npm package.
2479
2527
 
2480
- Stored short links are moving out of core this way too: a future
2481
- `urlcode-dynamic-link` package (mount-based, like `auth`/`admin`, not yet
2482
- published) will own the durable link store, its CLI and management API. Core
2483
- no longer has a native `link` handler or a `dynamicLinks` project flag.
2528
+ Stored short links moved out of core this way too: a `urlcode-dynamic-link`
2529
+ package (mount-based, like `auth`/`admin`) owned the durable link store, its CLI
2530
+ and management API. That package has since been retired and unpublished, so no
2531
+ supported stored-link extension ships today. Core no longer has a native `link`
2532
+ handler or a `dynamicLinks` project flag.
2484
2533
 
2485
2534
  A project declares versioned configuration and exclusive route mounts:
2486
2535
 
@@ -2663,7 +2712,7 @@ project customizes an extension's *look* through `urlcode-ui`'s
2663
2712
  standard, or every extension author (and every project depending on one)
2664
2713
  either reinvents it or forks the extension. `urlcode-auth` and
2665
2714
  `urlcode-admin` independently hit this gap (auth/#35, admin/#32); auth's own
2666
- [SPIKE-AUTH.md](https://github.com/jimhoyd-com/urlcode-auth/blob/main/docs/SPIKE-AUTH.md)
2715
+ [SPIKE-AUTH.md](https://github.com/jimhoyd-com/urlcode/blob/main/packages/auth/docs/SPIKE-AUTH.md)
2667
2716
  already scoped a shape for `onSignUp`/`beforeRegister`/`onDelete`.
2668
2717
 
2669
2718
  **The pattern.** An extension with lifecycle events an author judges worth
@@ -2727,7 +2776,16 @@ each half of a hook's `sandbox: true` opt-in has its own answer:
2727
2776
  or provided: this is ordinary first-party project code, and the
2728
2777
  extension's own `activate()` already has `ExtensionActivation.root` to
2729
2778
  resolve the hook's `source` against and can `import()` it directly, the
2730
- same way any trusted `function`/`middleware` route does.
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.
2731
2789
  - **Sandboxed (`sandbox: true`).** `@jimhoyd/urlcode/sandbox` exports
2732
2790
  `SandboxPool`, the same QuickJS/worker-thread engine that backs a
2733
2791
  sandboxed `function`/`middleware` route — the identical module-allowlist
@@ -2797,16 +2855,15 @@ boundary, not a JavaScript sandbox or an independent security review.
2797
2855
 
2798
2856
  ### Scaffolding with `init --with`
2799
2857
 
2800
- `urlcode init <directory> --with auth,admin` produces the layered site the
2858
+ `urlcode init <directory> --with ui,auth,admin` produces the layered site the
2801
2859
  [framework page](https://github.com/jimhoyd-com/urlcode/blob/main/docs/FRAMEWORK.md#the-composition-contract) describes in one
2802
2860
  command: the starter under `<directory>/app/`, one `host.mjs`, one `README.md`,
2803
2861
  and each extension's own operator files. Core never bundles or imports the
2804
2862
  extension packages at build time; at run time it resolves
2805
2863
  `@jimhoyd/urlcode-<name>` for each name with Node's package resolution from
2806
- the invoking directory (so `npm install @jimhoyd/urlcode-auth` in that
2807
- directory, from npm where the packages are published as `0.1.0-alpha.x`
2808
- prereleases, is the normal path and what makes `--with auth` work), imports
2809
- the package and calls its
2864
+ the invoking directory. Install a compatible package set there, as shown in
2865
+ [the framework guide](https://github.com/jimhoyd-com/urlcode/blob/main/docs/FRAMEWORK.md#the-composition-contract), before using
2866
+ `--with ui,auth`; UI must activate before auth. Core imports the package and calls its
2810
2867
  `scaffold` export with this request:
2811
2868
 
2812
2869
  ```ts
@@ -2862,6 +2919,37 @@ Assembly rules, in `--with` order:
2862
2919
  review the project and pin it explicitly; the host is generated to require
2863
2920
  the pin, never to compute it.
2864
2921
 
2922
+ #### Recorded versions
2923
+
2924
+ `init --with` also writes `<directory>/package.json`: a private manifest that
2925
+ pins, at exactly the version that was resolved, the running runtime, every
2926
+ extension named in `--with`, and every package those extensions declare in
2927
+ `peerDependencies` (so `@jimhoyd/urlcode-ui` is pinned for an `auth,admin` site
2928
+ although nobody named it). Before anything is written, the whole set is checked
2929
+ against every declared peer range; an incompatible combination or a missing
2930
+ required peer refuses and names it, leaving no directory behind.
2931
+
2932
+ Nothing is installed. The generated site has no `node_modules` and no
2933
+ `package-lock.json` until you run `npm install` in it yourself, which the
2934
+ command and the generated README both state as the next step. Reproducibility
2935
+ comes from that install, not from generation.
2936
+
2937
+ - `--no-manifest` generates the site without a `package.json`, for a site whose
2938
+ dependencies are managed elsewhere. Plain `urlcode init` is unchanged and
2939
+ still writes no manifest; add `--manifest` to pin the runtime for a
2940
+ route-only project too.
2941
+ - `--pin <package>=<specifier>` records a specifier instead of the resolved
2942
+ version, for local tarball or offline development
2943
+ (`--pin @jimhoyd/urlcode-auth=file:/abs/urlcode-auth-0.1.0-alpha.6.tgz`). A
2944
+ package installed from a local path or tarball is detected from npm's own
2945
+ install record and pinned by that path without any flag; the README says so,
2946
+ because such a pin only reproduces where that path exists.
2947
+
2948
+ There is no upgrade command. Moving a generated project to newer versions today
2949
+ means editing its `package.json` and re-running `npm install` yourself; nothing
2950
+ in this runtime selects a newer tested set, shows the change, or updates a
2951
+ lockfile for you.
2952
+
2865
2953
  Serving the result is the usual explicit host binding:
2866
2954
 
2867
2955
  ```sh
@@ -2870,6 +2958,289 @@ urlcode validate --project app --host-file "$PWD/host.mjs" --origin https://site
2870
2958
 
2871
2959
  ---
2872
2960
 
2961
+ # Composing a site from ui, auth and admin
2962
+
2963
+ Source: https://github.com/jimhoyd-com/urlcode/blob/main/docs/COMPOSING-A-SITE.md
2964
+
2965
+ One command produces a site that already has accounts, an administration
2966
+ console and a presentation kit wired together:
2967
+
2968
+ ```sh
2969
+ npm install @jimhoyd/urlcode @jimhoyd/urlcode-ui @jimhoyd/urlcode-auth @jimhoyd/urlcode-admin
2970
+ urlcode init site --with ui,auth,admin
2971
+ ```
2972
+
2973
+ This page is the map of what you may then change, and with which tool. It
2974
+ covers three different activities that are easy to confuse:
2975
+
2976
+ | You want to | Use | Code? |
2977
+ |---|---|---|
2978
+ | Change what an extension is configured to do | the `config` block in `app/urlcode.yaml` | none |
2979
+ | Change how its screens look or read | files under `<site>/ui/` | none (templates are data) |
2980
+ | Run your own logic at one of its lifecycle points | a project function named from that `config` | first-party project JavaScript |
2981
+ | Add a capability none of the three provides | a new extension package | TypeScript against the runtime contract |
2982
+
2983
+ Work down that list, not up. The [declarative-first
2984
+ principle](https://github.com/jimhoyd-com/urlcode/blob/main/docs/PROJECT-DIRECTION.md#design-principle-declarative-first) applies
2985
+ here as much as it does to routes: reach for the next row only when the one
2986
+ above it cannot express the requirement.
2987
+
2988
+ ### What `--with ui,auth,admin` generates
2989
+
2990
+ - `site/app/` — the route project: `urlcode.yaml` with an `extensions` block
2991
+ per package, and `routes/extensions.yaml` holding `/assets/ui/*`,
2992
+ `/account/*`, `/private` and `/admin/*`.
2993
+ - `site/host.mjs` — the operator host module, the one place that holds code.
2994
+ It builds the kit with `createUiExtension`, passes the returned object into
2995
+ `authExtension` and `adminExtension`, and lists `ui.registration` first.
2996
+ - `site/ui/` — `copy/`, `templates/` and `extra.css`, the project's
2997
+ presentation overrides, beside the host and **outside** `app/`.
2998
+ - `site/operator-service.mjs`, `site/data/` — auth's operator service and its
2999
+ private key material, mode `0600`.
3000
+ - `site/README.md` — the merged next steps, environment table and the project
3001
+ revision to review and pin.
3002
+
3003
+ Nothing about that wiring is manual any more. The generated host registers
3004
+ `authCatalogue` as a copy source and both `authUiTemplates` and
3005
+ `adminUiTemplates` as template namespaces, because auth and admin render only
3006
+ through the kit and refuse to activate without their own templates present.
3007
+
3008
+ #### Supported combinations
3009
+
3010
+ `--with` order is the activation order, and the kit must be active before
3011
+ anything that renders through it.
3012
+
3013
+ | `--with` | Result |
3014
+ |---|---|
3015
+ | `ui` | Kit only; the host wires no peer catalogue or templates. |
3016
+ | `ui,auth` | Accounts on `/account/*`, rendered through the kit. |
3017
+ | `ui,auth,admin` | The full composition above. |
3018
+ | `auth` or `auth,admin` | Refused: the scaffold names the missing `ui`. |
3019
+ | `auth,admin,ui` | Refused: `ui` must come before the extensions it renders. |
3020
+ | `admin` without `auth` | Refused: admin reuses auth's service, CSRF key and revision. |
3021
+
3022
+ Every refusal happens before anything is written, and leaves no directory
3023
+ behind. There is no auth-without-ui or admin-without-ui configuration in this
3024
+ revision: the UI primitive fallback was retired, so the kit is the only render
3025
+ path (see [OPEN-DECISIONS.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/OPEN-DECISIONS.md)).
3026
+
3027
+ ### Declarative configuration
3028
+
3029
+ Each package owns one `extensions.<name>` block. The block itself is core
3030
+ schema (`version` plus `config`); what may go inside `config` is the
3031
+ package's own JSON Schema, which you can print rather than guess:
3032
+
3033
+ ```sh
3034
+ urlcode extensions --project ./site/app --host-file "$PWD/site/host.mjs" --json
3035
+ ```
3036
+
3037
+ The same report is the MCP tool `get_extensions`, and it is the authoritative
3038
+ answer for both the configuration schema and the per-route policy schema. The
3039
+ generated site starts from something like this:
3040
+
3041
+ ```yaml
3042
+ version: "1"
3043
+ extensions:
3044
+ ui:
3045
+ version: "1"
3046
+ config:
3047
+ theme:
3048
+ name: My Site
3049
+ languages: [en]
3050
+ copy: ui/copy
3051
+ templates: ui/templates
3052
+ stylesheet: ui/extra.css
3053
+ auth:
3054
+ version: "1"
3055
+ config:
3056
+ registration: "off"
3057
+ admin:
3058
+ version: "1"
3059
+ config: {}
3060
+ ```
3061
+
3062
+ Routes mount an extension, and policies require one:
3063
+
3064
+ ```yaml
3065
+ routes:
3066
+ /assets/ui/*:
3067
+ extension: ui
3068
+ methods: [GET, HEAD]
3069
+ /account/*:
3070
+ extension: auth
3071
+ methods: [GET, HEAD, POST]
3072
+ /admin/*:
3073
+ extension: admin
3074
+ methods: [GET, HEAD, POST]
3075
+ /private:
3076
+ respond:
3077
+ text: Signed in
3078
+ policies:
3079
+ extensions:
3080
+ auth: {}
3081
+ ```
3082
+
3083
+ See [EXTENSIONS.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md) for the `auth` route short form, extension
3084
+ middleware, and the host-file trust boundary.
3085
+
3086
+ ### Presentation overrides
3087
+
3088
+ The `ui` config's `copy`, `templates` and `stylesheet` paths point at the
3089
+ project's own directories. Nothing here forks a package.
3090
+
3091
+ | Override | File | Effect |
3092
+ |---|---|---|
3093
+ | Wording and translation | `ui/copy/<locale>.json` | Replaces catalogue ids, including ids the auth and admin packages own. Listed in `languages`. |
3094
+ | 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
+ | Styling | `ui/extra.css` | Appended after the kit stylesheet; `{file, replace: true}` replaces it instead. |
3096
+ | Colours, logo, favicon, radius, font | the `theme` block | Declarative; no file needed. |
3097
+
3098
+ A template is data in the kit's own language. It cannot add a script, change
3099
+ what a form validates, or change what a page sends in headers — so an override
3100
+ cannot weaken the screen it restyles. Stylesheets containing `@import`,
3101
+ `script`, `javascript:` or `expression(` are refused.
3102
+
3103
+ Names, coverage and what the runtime will actually load:
3104
+
3105
+ ```sh
3106
+ npx urlcode-ui list --project ./site --extensions @jimhoyd/urlcode-auth,@jimhoyd/urlcode-admin
3107
+ npx urlcode-ui doctor --project ./site --extensions @jimhoyd/urlcode-auth,@jimhoyd/urlcode-admin --copy ui/copy --templates ui/templates --stylesheet ui/extra.css
3108
+ npx urlcode-ui eject auth/sign-in --out ./site/ui/templates --project ./site --extensions @jimhoyd/urlcode-auth
3109
+ npx urlcode-ui copy --missing fr --project ./site --extensions @jimhoyd/urlcode-auth --copy ui/copy --languages en,fr
3110
+ ```
3111
+
3112
+ `eject` copies the shipped source so an override starts from what ships and
3113
+ never overwrites an existing file. `ui/` lives outside `app/`, so editing copy
3114
+ or templates does **not** change the project revision and does not require
3115
+ re-pinning `PROJECT_SHA256`.
3116
+
3117
+ **Name the packages that ship the other namespaces.** `urlcode-ui` is this
3118
+ kit alone until `--extensions` names them. Each package is resolved from
3119
+ `--project` with Node package resolution and imported for the namespace it
3120
+ exports; one that is not installed there is skipped with a note, so the
3121
+ command still runs. The site's `host.mjs` is never read: it builds services
3122
+ and reads secrets at its top level, and a read-only `list` or `doctor` must
3123
+ not run it. With the packages named:
3124
+
3125
+ - `list` shows the `auth/*` and `admin/*` names beside the kit's own, each
3126
+ with its origin, and `eject auth/sign-in` copies the shipped source.
3127
+ - `doctor` reports an `expected` view model for an extension template, so its
3128
+ `behind` flag tells you when an override of one has fallen behind what
3129
+ ships. Its `extensions` field names the namespaces the report covers, so a
3130
+ report built without a peer is visible as such.
3131
+ - `preview auth/sign-in` renders the extension's own sample view model.
3132
+ - `copy --missing` skeletons cover the auth ids the account screens use.
3133
+ Admin-owned `adminUi.*` ids are deliberately not offered: admin composes
3134
+ its catalogue onto the kit's presentation rather than registering it there,
3135
+ and those translations do not currently reach the console
3136
+ ([#227](https://github.com/jimhoyd-com/urlcode/issues/227)).
3137
+
3138
+ `urlcode init <directory> --with ui,auth,admin` writes these commands into the
3139
+ generated README with the flag already set. `@jimhoyd/urlcode-ui` depends on
3140
+ neither peer; the operator names them.
3141
+
3142
+ Overrides of extension templates and of extension-owned catalogue ids reach
3143
+ the rendered screens, which is what the regression test below asserts.
3144
+
3145
+ ### Project functions: lifecycle hooks
3146
+
3147
+ A hook is your own function, named from the extension's `config`, that the
3148
+ extension calls at a lifecycle point it defines. It uses the same source shape
3149
+ a `function` route uses — a bare path, or `{source, export}` — resolved
3150
+ relative to the route project.
3151
+
3152
+ ```yaml
3153
+ extensions:
3154
+ auth:
3155
+ version: "1"
3156
+ config:
3157
+ registration: open
3158
+ hooks:
3159
+ beforeRegister:
3160
+ source: ./hooks/registration-rule.mjs
3161
+ export: default
3162
+ onSignUp: ./hooks/on-signup.mjs
3163
+ ```
3164
+
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.
3168
+
3169
+ Hooks are first-party project code and run **trusted and in-process**, the
3170
+ 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.
3174
+
3175
+ #### `@jimhoyd/urlcode-auth`
3176
+
3177
+ | Hook | Input | Returns | Called |
3178
+ |---|---|---|---|
3179
+ | `beforeRegister` | `{email, profile?}` | `{allow: boolean, reason?}` | Before an account is created, on `POST /account/register` and on `POST /account/signup/begin`. |
3180
+ | `onSignUp` | `{accountId, email}` | ignored | After a genuinely new account is created — on `/account/register`, and on `/account/signup/complete` only when that completion created an account rather than signing an existing one in. |
3181
+ | `onDelete` | `{accountId, email}` | ignored | After the account owner's own deletion is scheduled. Not on an administrator-initiated deletion, and not on the background purge when the grace period ends. |
3182
+
3183
+ #### `@jimhoyd/urlcode-admin`
3184
+
3185
+ | Hook | Input | Returns | Called |
3186
+ |---|---|---|---|
3187
+ | `beforeRoleChange` | `{accountId, currentRoles, requestedRoles, actorId, reason}` | `{allow: boolean, reason?}` | Before roles are applied, after the administrator's permission check. A veto means the auth service is never asked. |
3188
+ | `onRegistrationApproved` | `{requestId, accountId, email, actorId, reason}` | ignored | After a registration request is approved. |
3189
+ | `onAccountStatusChanged` | `{accountId, status, actorId, reason}` | ignored | After an account is locked or unlocked. |
3190
+
3191
+ #### Verdicts and failure
3192
+
3193
+ - **A veto is explicit.** A pre-action hook allows only by returning
3194
+ `allow: true`. `allow: false`, or no verdict at all, rejects the operation
3195
+ with `403` and the hook's own `reason`, or a generic message when it gave
3196
+ none. Nothing is written. A hook that *throws* has not returned a verdict:
3197
+ the operation is still refused, but as a generic `500`, so return a verdict
3198
+ rather than throwing when you mean to deny.
3199
+ - **Broken hooks fail at activation, not at the first request.** A missing
3200
+ module, a source path escaping the project, an export that is not a
3201
+ function, or `sandbox: true` all throw while the extension activates, naming
3202
+ the hook. The site does not start.
3203
+ - **A post-action hook cannot undo anything.** `onSignUp`,
3204
+ `onDelete`, `onRegistrationApproved` and `onAccountStatusChanged` run after
3205
+ the operation has committed. Throwing from one replaces the success response
3206
+ with a `500` while the account, approval or status change stands. There is
3207
+ no retry and no rollback. Keep them non-throwing: catch your own errors and
3208
+ queue the work instead of failing the request.
3209
+ - **A hook's message is not a channel to the browser.** Only a pre-action
3210
+ `reason` is shown. An uncaught error surfaces as a generic failure.
3211
+ - **An edited hook needs a restart.** Activation re-imports the hook's entry
3212
+ module, so a reload picks up an edit to that file — but modules it imports
3213
+ stay on Node's module cache, exactly as for trusted route functions.
3214
+
3215
+ ### TypeScript: implementing a new extension
3216
+
3217
+ Only write an extension when a capability is genuinely absent — not to
3218
+ customize one of the three above. An extension is an operator-installed
3219
+ package whose host object core activates; it is named in `host.mjs`, never
3220
+ in YAML. The contract, the activation inputs, `ExtensionActivation.root`,
3221
+ credential headers and the `projectSha256` pin are in
3222
+ [EXTENSIONS.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md) and [TYPESCRIPT.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/TYPESCRIPT.md); the
3223
+ `scaffold` export that makes a package work with `init --with` is in
3224
+ [EXTENSIONS.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md#scaffolding-with-init---with).
3225
+
3226
+ If its screens should be themeable the same way auth's and admin's are, it
3227
+ also exports a template namespace (and, if it ships English wording, a
3228
+ catalogue) for a host to pass to `createUiExtension`. That is what makes
3229
+ `ui/templates/<yourname>/<screen>.html` work in a consumer project without a
3230
+ fork.
3231
+
3232
+ ### What this page does not claim
3233
+
3234
+ The composition, the refusals and the override path are exercised by
3235
+ `test/workspace-scaffold.integration.ts`, which runs `init --with` against the
3236
+ built packages, drops a template and a copy catalogue into the generated
3237
+ `ui/` directory and asserts both reach a rendered auth screen and a rendered
3238
+ admin screen. That runs in-process against the generated host: no HTTP
3239
+ listener, TLS proxy, browser or deployed site is exercised, and no published
3240
+ npm tarball is checked against this checkout.
3241
+
3242
+ ---
3243
+
2873
3244
  # Function execution: trusted by default, sandboxed opt-in
2874
3245
 
2875
3246
  Source: https://github.com/jimhoyd-com/urlcode/blob/main/docs/FUNCTION-SECURITY.md
@@ -2914,6 +3285,35 @@ API is all it has, so its binding grant *is* effectively its whole reach into
2914
3285
  the environment. Trusted code's reach is not bounded that way; treat the
2915
3286
  grant as scoping `context`, not as scoping the process.
2916
3287
 
3288
+ ### Migrating to the trusted default
3289
+
3290
+ If you are upgrading a project from a release before this change shipped:
3291
+ **every existing `function` and `middleware` route silently changes execution
3292
+ mode**, from sandboxed to trusted, unless it already has (or you add)
3293
+ `sandbox: true`. This is a real behavior change on upgrade, not a
3294
+ documentation update — a route that used to run with no filesystem or network
3295
+ access will, after the upgrade, run with full Node access unless you opt it
3296
+ back into the sandbox.
3297
+
3298
+ Before upgrading:
3299
+
3300
+ - List every `function` and `middleware` route in the project.
3301
+ - For each one, decide whether you fully trust that code to run in-process
3302
+ with full Node/filesystem/network access — the same trust you would extend
3303
+ to any other code you deploy to that server.
3304
+ - Add `sandbox: true` explicitly to any route whose code you do not fully
3305
+ trust, that processes input from a source you don't control, or that handles
3306
+ a secret binding you want isolated — before you upgrade, not after.
3307
+ - Routes you do want running trusted need no change; that is now the default.
3308
+
3309
+ The change moves the sandbox from an unconditional guarantee to an explicit,
3310
+ per-route choice, mainly for performance: the previous blanket sandbox capped
3311
+ concurrency at two workers with no queue shared across every function route on
3312
+ the server, which does not scale to real concurrent traffic. It also brings
3313
+ first-party code in line with how the rest of the Node ecosystem treats
3314
+ deployed application code. The sandbox itself is unchanged for routes that opt
3315
+ into it; only the default for routes that declare neither option has changed.
3316
+
2917
3317
  ### What "sandboxed" (`sandbox: true`) still guarantees
2918
3318
 
2919
3319
  - Function sources are parsed/snapshotted without importing them into Node.
@@ -2990,6 +3390,21 @@ it is handed *through `context`*. It does not change what the code can go get
2990
3390
  on its own once it is running; see "binding grants are unaffected" above for
2991
3391
  that distinction.
2992
3392
 
3393
+ ### Trusted code, not trusted requests
3394
+
3395
+ "Trusted" describes the code's authorship — first-party project code you
3396
+ reviewed and deployed — not the requests it handles. Every request, in either
3397
+ mode, still carries client-controlled path, query, header and body data that is
3398
+ exactly as adversarial as it always was. Running trusted means that code
3399
+ executes with full Node access if it mishandles that input; it does not mean
3400
+ the input itself became safe to trust. Declare `parameters` and `request.body`
3401
+ validation in YAML, check `args` and any other request data again inside
3402
+ function/middleware code, and implement your own authentication and
3403
+ authorization — no route, sandboxed or trusted, adds automatic auth.
3404
+ `sandbox: true` narrows what a bug or an unreviewed dependency in the *code*
3405
+ can do with that same request data; it is not a substitute for validating or
3406
+ authenticating the request itself.
3407
+
2993
3408
  ### Granting selected bindings
2994
3409
 
2995
3410
  An application may request a named binding in YAML, but only an operator can