@jimhoyd/urlcode 0.4.0-alpha.3 → 0.4.2

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 (82) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +10 -0
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/CONTRIBUTING.md +36 -0
  4. package/README.md +20 -15
  5. package/ROADMAP.md +25 -16
  6. package/dist/BUILD-MANIFEST.json +20 -19
  7. package/dist/authoring.js +15 -1
  8. package/dist/capability-query.js +0 -1
  9. package/dist/catalog.js +0 -1
  10. package/dist/cli.js +24 -7
  11. package/dist/config.js +1 -1
  12. package/dist/explain.js +1 -1
  13. package/dist/extensions.js +78 -1
  14. package/dist/http-response.js +1 -1
  15. package/dist/index.js +1 -0
  16. package/dist/init-with.js +36 -11
  17. package/dist/manifest.js +1 -1
  18. package/dist/mcp-authoring.js +2 -2
  19. package/dist/mcp.js +1 -1
  20. package/dist/policies/cache.js +2 -2
  21. package/dist/policy.js +16 -0
  22. package/dist/project-dependencies.js +305 -0
  23. package/dist/readiness.js +5 -1
  24. package/dist/runtime.js +1 -1
  25. package/dist/tooling.js +2 -1
  26. package/dist/trusted-functions.js +4 -5
  27. package/dist/types/authoring.d.ts +9 -1
  28. package/dist/types/capability-query.d.ts +0 -1
  29. package/dist/types/catalog.d.ts +0 -4
  30. package/dist/types/config.d.ts +1 -9
  31. package/dist/types/explain.d.ts +0 -1
  32. package/dist/types/extensions.d.ts +58 -0
  33. package/dist/types/http-response.d.ts +0 -1
  34. package/dist/types/index.d.ts +1 -0
  35. package/dist/types/init-with.d.ts +7 -13
  36. package/dist/types/manifest.d.ts +0 -1
  37. package/dist/types/project-dependencies.d.ts +78 -0
  38. package/dist/types/readiness.d.ts +3 -0
  39. package/dist/types/tooling.d.ts +1 -0
  40. package/dist/types/trusted-functions.d.ts +1 -4
  41. package/docs/AI-AUTHORING.md +10 -5
  42. package/docs/AWS.md +9 -0
  43. package/docs/CI-FOLLOWUP-2026-09-19.md +1 -1
  44. package/docs/CODEBASE-AUDIT-2026-09-20.md +6 -0
  45. package/docs/COMPOSING-A-SITE.md +287 -0
  46. package/docs/CONTAINER-PROMOTION.md +74 -0
  47. package/docs/DEVELOPMENT-PIPELINE.md +242 -119
  48. package/docs/EXTENSIONS.md +88 -93
  49. package/docs/FRAMEWORK.md +45 -30
  50. package/docs/FUNCTION-SECURITY.md +5 -8
  51. package/docs/INSTALL.md +13 -8
  52. package/docs/MIDDLEWARE.md +10 -4
  53. package/docs/OPEN-DECISIONS.md +64 -99
  54. package/docs/READINESS.md +8 -4
  55. package/docs/README.md +18 -13
  56. package/docs/RELEASE-0.4.1.md +73 -0
  57. package/docs/RELEASE-0.4.2.md +30 -0
  58. package/docs/RELEASE-READINESS.md +40 -11
  59. package/docs/RELEASE-SECURITY.md +33 -14
  60. package/docs/SPECIFICATION.md +5 -1
  61. package/docs/SPIKE-CORE-LAYERING.md +1 -1
  62. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +9 -13
  63. package/docs/STARTERS.md +17 -5
  64. package/docs/TOOLING.md +7 -5
  65. package/docs/VERCEL.md +10 -2
  66. package/docs/VERSION-ALIGNMENT.md +50 -8
  67. package/docs/archive/2026-09-19/ROADMAP.md +1 -0
  68. package/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md +1 -0
  69. package/docs/{SPIKE-LAMBDA-COMPILE.md → archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md} +168 -12
  70. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +2 -0
  71. package/docs/archive/2026-09-20/OPEN-DECISIONS-COMPLETED.md +116 -0
  72. package/docs/archive/README.md +2 -0
  73. package/docs/yaml/functions.md +10 -2
  74. package/docs/yaml/middleware.md +5 -3
  75. package/examples/cookbook/middleware/envelope.mjs +4 -2
  76. package/llms-full.txt +458 -143
  77. package/llms.txt +2 -1
  78. package/package.json +8 -5
  79. package/packaging/claude-plugin/.claude-plugin/plugin.json +1 -1
  80. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +10 -0
  81. package/recipes/middleware/middleware/envelope.mjs +4 -2
  82. package/skills/urlcode/SKILL.md +8 -1
@@ -183,29 +183,18 @@ Node adapter conformance is not a live-provider deployment claim.
183
183
 
184
184
  ## Project-level lifecycle hooks
185
185
 
186
- `authorize` and `middleware` let core call *into* an extension. They do not
187
- let a project hand its own code *to* an extension to run at a defined point.
188
- That gap matters once an extension has meaningful lifecycle events
189
- registration, deletion, an administrative action, a link resolution, and so
190
- on. Presentation already has a standard layering mechanism for this: a
191
- project customizes an extension's *look* through `urlcode-ui`'s
192
- `copy`/`extra.css`/`templates` without forking it. Behavior needs the same
193
- standard, or every extension author (and every project depending on one)
194
- either reinvents it or forks the extension. `urlcode-auth` and
195
- `urlcode-admin` independently hit this gap (auth/#35, admin/#32); auth's own
196
- [SPIKE-AUTH.md](https://github.com/jimhoyd-com/urlcode-auth/blob/main/docs/SPIKE-AUTH.md)
197
- already scoped a shape for `onSignUp`/`beforeRegister`/`onDelete`.
198
-
199
- **The pattern.** An extension with lifecycle events an author judges worth
200
- exposing should let the project name its own function in the extension's own
201
- `config`, using the same source shape `function`/`middleware` routes already
202
- use (a string path, or `{source, export, args}` — `schemas/urlcode.schema.json`),
203
- and add its own `sandbox` boolean next to it (below). The extension's own
204
- `activate()` reads that config, and its own runtime dispatch — not a new core
205
- primitive, an ordinary call the extension package makes with the request
206
- context it already has — invokes the named function at the lifecycle point
207
- it defines, with a typed input and a typed verdict the extension's own
208
- schema documents. For example, an auth-style extension might declare:
186
+ Extensions expose project customization points through the core hook primitive.
187
+ Each registration publishes `hooks`, a machine-readable list containing the
188
+ hook name, whether it is a value-transforming `filter` or side-effect `action`,
189
+ its description and its input/output JSON Schemas. The extension embeds
190
+ `extensionHooksSchema(contracts)` in its configuration schema and calls
191
+ `loadExtensionHooks(config.hooks, contracts, context)` during activation.
192
+ Core then enforces the common source/export shape, project-root confinement,
193
+ known names, eager module/export validation, input/output schemas and reload
194
+ cache busting. Hook entry bytes participate in the project revision, so editing
195
+ a hook invalidates the operator's extension pin.
196
+
197
+ Projects select those declared hooks in the extension's own configuration:
209
198
 
210
199
  ```yaml
211
200
  extensions:
@@ -221,76 +210,51 @@ extensions:
221
210
  ```
222
211
 
223
212
  with `beforeRegister` called before an account is created, given a typed
224
- `{email, traits}`-shaped input and returning a typed verdict (`{allow: true}`
213
+ `{email, profile?}` input and returning a typed verdict (`{allow: true}`
225
214
  or `{allow: false, reason}`), and `onSignUp` called after, for side effects
226
- such as provisioning a workspace. The exact hook names, input/verdict shapes
227
- and invocation points are the extension's own design `hooks` is not a core
228
- schema key — but the source/export/sandbox shape, and the fact that this is
229
- project code the extension calls rather than an operator callback in
230
- `host.mjs`, should be consistent across extensions so an author who has
231
- learned one has learned the pattern.
232
-
233
- **Trust and execution mode.** Project-level lifecycle hooks are first-party
234
- project code, the same category as any `function`/`middleware` route, and
235
- follow the same trusted-by-default rule with no special case
236
- (docs/SPIKE-DEFAULT-TRUST-MODEL.md, [FUNCTION-SECURITY.md](FUNCTION-SECURITY.md)):
237
- trusted, in-process execution by default; a project sets `sandbox: true` on
238
- a given hook to opt that hook into isolated QuickJS/WASM execution, exactly
239
- the mechanism `function`/`middleware` routes already use and for the same
240
- reason the hook's own code, not the trustworthiness of whatever triggered
241
- it, is what calls for isolation (docs/AI-AUTHORING.md's "Deciding when a
242
- route needs `sandbox: true`"). This was raised as an open question — whether
243
- a lifecycle hook should always run sandboxed because it makes a
244
- security-relevant decision and settled the other way: uniformity with the
245
- rest of the trust model was chosen over hardwiring isolation for lifecycle
246
- hooks specifically, the same "no special case" call already made between
247
- `function` and `middleware` trust. A `beforeRegister` hook enforcing "only
248
- `@acme.com` may register" is the project's own governance rule over its own
249
- signup flow; it is not more dangerous than any other route the project
250
- wrote, and does not get a different default.
251
-
252
- Core's own trusted/sandboxed dispatch (`TrustedFunctions`/`FunctionPool`,
253
- `src/runtime.ts`) is wired to route dispatch, not exposed to extensions — but
254
- each half of a hook's `sandbox: true` opt-in has its own answer:
255
-
256
- - **Trusted (the default, no `sandbox: true`).** No core primitive is needed
257
- or provided: this is ordinary first-party project code, and the
258
- extension's own `activate()` already has `ExtensionActivation.root` to
259
- resolve the hook's `source` against and can `import()` it directly, the
260
- same way any trusted `function`/`middleware` route does. Do that import
261
- with a per-activation cache-busting query, the way core's own trusted
262
- route activation does (`src/trusted-functions.ts`): Node's ESM loader
263
- caches a resolved module forever by URL, so a plain `import()` of the
264
- unchanged file URL makes a second activation in the same process keep
265
- serving the hook code that was on disk at the first one
266
- (jimhoyd-com/urlcode#198). Only the hook's **entry** module is refreshed
267
- this way — modules the hook itself imports stay on Node's module cache,
268
- the same limitation the trusted route path has, so a change to a hook's
269
- own dependency still needs a process restart.
270
- - **Sandboxed (`sandbox: true`).** `@jimhoyd/urlcode/sandbox` exports
271
- `SandboxPool`, the same QuickJS/worker-thread engine that backs a
272
- sandboxed `function`/`middleware` route — the identical module-allowlist
273
- walk, memory/stack limits, two-layer deadline enforcement, `maxBytes` and
274
- response-shape validation, with no separate or weaker engine for
275
- extensions. It takes an explicit list of `{source, export}` entries
276
- (resolve a hook's `source` string with the re-exported `functionFile()`,
277
- the same resolution/validation a native route's `source` gets) instead of
278
- anything route/YAML-shaped, and `execute({entry, chain}, request, context)`
279
- in place of a `FunctionRoute`. There is no "trusted" mode exported
280
- alongside it — `SandboxPool` is only ever the isolated path; see
281
- [FUNCTION-SECURITY.md](FUNCTION-SECURITY.md) and
282
- [TYPESCRIPT.md](TYPESCRIPT.md) for the full contract.
283
-
284
- An extension honoring a hook's `sandbox: true` is expected to actually
285
- isolate that invocation through `SandboxPool` now that the primitive exists
286
- (or document plainly that it does not yet, rather than accepting the field
287
- and silently running it trusted) — say which, in the extension's own docs,
288
- so an author reading them is not misled about what opt-in exists.
215
+ such as provisioning a workspace. Hook names and lifecycle timing remain the
216
+ extension's domain, while their declaration, loading and discovery are shared.
217
+
218
+ Hooks are first-party project code and run trusted in-process by default, with
219
+ full Node access, like trusted `function` and `middleware` routes. Contract v1
220
+ does not define an arbitrary-value sandbox hook protocol. A hook reference with
221
+ `sandbox: true` is rejected during activation rather than silently run trusted.
222
+ Only the entry module is refreshed during reactivation; its imported dependencies
223
+ remain in Node's module cache until restart.
224
+
225
+ The UI extension exposes `transformView`, a synchronous filter called before a
226
+ named kit template renders. It receives `{template, view}` and returns the view
227
+ model to render. Use copy, templates, theme and CSS for ordinary presentation
228
+ changes; use this hook for project-specific computed view data that those
229
+ declarative layers cannot express.
230
+
231
+ ## Building an extension
232
+
233
+ An extension package should export a registration factory and, when it supports
234
+ `urlcode init --with`, a side-effect-free `scaffold` function. The registration:
235
+
236
+ 1. Declares its logical name, contract version, supported targets, exact project
237
+ revision pin and strict configuration/policy schemas.
238
+ 2. Publishes every project hook through `hooks` and reuses
239
+ `extensionHooksSchema` plus `loadExtensionHooks`; it does not implement its
240
+ own path resolver or dynamic-import cache.
241
+ 3. Activates all configuration, files, services and hooks before serving a
242
+ request. Invalid or stale configuration fails activation.
243
+ 4. Returns `handle` for mounts and optionally `authorize`/`middleware` for route
244
+ policies. It closes resources it owns.
245
+ 5. Keeps credentials, storage and provider setup in the operator host. Project
246
+ YAML contains logical configuration and project-relative hook references.
247
+
248
+ Consumers install the package, declare its YAML block and mounts/policies, and
249
+ register it in `host.mjs`. They modify it through declared configuration,
250
+ presentation layers and hooks. A fork is reserved for changing behavior the
251
+ extension has not exposed; that is evidence for a new declarative field or hook.
252
+ See [Composing a site](COMPOSING-A-SITE.md) for the complete ui/auth/admin example.
289
253
 
290
254
  ## Discovering schemas
291
255
 
292
256
  Each registration carries the JSON Schemas that validate its `config` block and
293
- its per-route policy requirements. `urlcode extensions` prints them together with
257
+ its per-route policy requirements, plus its hook contracts. `urlcode extensions` prints them together with
294
258
  the project's own declarations so an author can see what a mount accepts:
295
259
 
296
260
  ```sh
@@ -299,6 +263,7 @@ urlcode extensions --project ./site --host-file /absolute/operator/host.mjs [--j
299
263
 
300
264
  For every registration in the host file it reports the name, contract version,
301
265
  targets, credential headers, configuration schema, policy schema (if any),
266
+ declared hook names, kinds, descriptions and input/output schemas,
302
267
  whether the project declares it, whether its `projectSha256` matches the current
303
268
  revision, the routes that mount it and the routes whose policies require it.
304
269
  Declared names the host does not register are listed as unregistered. The command
@@ -336,16 +301,15 @@ boundary, not a JavaScript sandbox or an independent security review.
336
301
 
337
302
  ## Scaffolding with `init --with`
338
303
 
339
- `urlcode init <directory> --with auth,admin` produces the layered site the
304
+ `urlcode init <directory> --with ui,auth,admin` produces the layered site the
340
305
  [framework page](FRAMEWORK.md#the-composition-contract) describes in one
341
306
  command: the starter under `<directory>/app/`, one `host.mjs`, one `README.md`,
342
307
  and each extension's own operator files. Core never bundles or imports the
343
308
  extension packages at build time; at run time it resolves
344
309
  `@jimhoyd/urlcode-<name>` for each name with Node's package resolution from
345
- the invoking directory (so `npm install @jimhoyd/urlcode-auth` in that
346
- directory, from npm where the packages are published as `0.1.0-alpha.x`
347
- prereleases, is the normal path and what makes `--with auth` work), imports
348
- the package and calls its
310
+ the invoking directory. Install a compatible package set there, as shown in
311
+ [the framework guide](FRAMEWORK.md#the-composition-contract), before using
312
+ `--with ui,auth`; UI must activate before auth. Core imports the package and calls its
349
313
  `scaffold` export with this request:
350
314
 
351
315
  ```ts
@@ -401,6 +365,37 @@ Assembly rules, in `--with` order:
401
365
  review the project and pin it explicitly; the host is generated to require
402
366
  the pin, never to compute it.
403
367
 
368
+ ### Recorded versions
369
+
370
+ `init --with` also writes `<directory>/package.json`: a private manifest that
371
+ pins, at exactly the version that was resolved, the running runtime, every
372
+ extension named in `--with`, and every package those extensions declare in
373
+ `peerDependencies` (so `@jimhoyd/urlcode-ui` is pinned for an `auth,admin` site
374
+ although nobody named it). Before anything is written, the whole set is checked
375
+ against every declared peer range; an incompatible combination or a missing
376
+ required peer refuses and names it, leaving no directory behind.
377
+
378
+ Nothing is installed. The generated site has no `node_modules` and no
379
+ `package-lock.json` until you run `npm install` in it yourself, which the
380
+ command and the generated README both state as the next step. Reproducibility
381
+ comes from that install, not from generation.
382
+
383
+ - `--no-manifest` generates the site without a `package.json`, for a site whose
384
+ dependencies are managed elsewhere. Plain `urlcode init` is unchanged and
385
+ still writes no manifest; add `--manifest` to pin the runtime for a
386
+ route-only project too.
387
+ - `--pin <package>=<specifier>` records a specifier instead of the resolved
388
+ version, for local tarball or offline development
389
+ (`--pin @jimhoyd/urlcode-auth=file:/abs/urlcode-auth-0.1.0-alpha.6.tgz`). A
390
+ package installed from a local path or tarball is detected from npm's own
391
+ install record and pinned by that path without any flag; the README says so,
392
+ because such a pin only reproduces where that path exists.
393
+
394
+ There is no upgrade command. Moving a generated project to newer versions today
395
+ means editing its `package.json` and re-running `npm install` yourself; nothing
396
+ in this runtime selects a newer tested set, shows the change, or updates a
397
+ lockfile for you.
398
+
404
399
  Serving the result is the usual explicit host binding:
405
400
 
406
401
  ```sh
package/docs/FRAMEWORK.md CHANGED
@@ -7,26 +7,26 @@ claim here is implemented in the linked repository; nothing is roadmap.
7
7
 
8
8
  ## Four packages, one project shape
9
9
 
10
- | Package | Repository | What it adds | How a project declares it |
10
+ | Package | Source | What it adds | How a project declares it |
11
11
  |---|---|---|---|
12
- | `@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"` |
13
- | `@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 |
14
- | `@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 |
15
- | `@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 |
16
-
17
- The core is Apache-2.0 and released. The three extension packages are
18
- Apache-2.0 and published to npm as alphas (`@jimhoyd/urlcode-ui@0.1.0-alpha.4`,
19
- `@jimhoyd/urlcode-auth@0.1.0-alpha.2`, `@jimhoyd/urlcode-admin@0.1.0-alpha.2`,
20
- on core `0.4.0-alpha.1`). An alpha on npm is a distribution channel, not an
21
- endorsement: the source is complete, but independent review, deployment
22
- evidence and an accessibility assessment are still pending
12
+ | `@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"` |
13
+ | `@jimhoyd/urlcode-ui` | [`packages/ui`](../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 |
14
+ | `@jimhoyd/urlcode-auth` | [`packages/auth`](../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 |
15
+ | `@jimhoyd/urlcode-admin` | [`packages/admin`](../packages/admin) | Administration: users, sessions, roles, audit, registration approval, two-person cases, support impersonation, health | `extensions.admin` plus an `/admin/*` mount |
16
+
17
+ All four are Apache-2.0. The `0.4.1` release line aligns their stable versions;
18
+ check `npm run release:status` for publication progress. A stable npm channel is
19
+ not an independent assessment: review, deployment evidence and an accessibility
20
+ assessment are still pending
23
21
  ([issue 58](https://github.com/jimhoyd-com/urlcode/issues/58)). Their status
24
- files say exactly what is built: [auth](https://github.com/jimhoyd-com/urlcode-auth/blob/main/IMPLEMENTATION-STATUS.md),
25
- [admin](https://github.com/jimhoyd-com/urlcode-admin/blob/main/IMPLEMENTATION-STATUS.md),
26
- [ui](https://github.com/jimhoyd-com/urlcode-ui/blob/main/IMPLEMENTATION-STATUS.md).
27
- Which core version each package supports, how it declares that, and the order
28
- in which a core change reaches the downstream repositories are recorded in
29
- [core version alignment](VERSION-ALIGNMENT.md).
22
+ files say exactly what is built: [auth](../packages/auth/IMPLEMENTATION-STATUS.md),
23
+ [admin](../packages/admin/IMPLEMENTATION-STATUS.md),
24
+ [ui](../packages/ui/IMPLEMENTATION-STATUS.md).
25
+ The current version of each package is its own manifest, and the peer ranges it
26
+ declares are in that manifest too; do not read a version number out of this
27
+ page. How versions, channels and release tags line up is recorded in
28
+ [package and channel alignment](VERSION-ALIGNMENT.md), and `npm run
29
+ release:status` reports the live registry and tag state.
30
30
 
31
31
  ## The ladder
32
32
 
@@ -43,8 +43,10 @@ Each rung's YAML is valid on every rung above it.
43
43
  3. **Functions and middleware.** `function` routes and ordered `middleware`
44
44
  in JavaScript, trusted and in-process by default; a route declaring
45
45
  `sandbox: true` runs isolated instead (QuickJS inside WebAssembly, fresh
46
- heap per call, no Node, filesystem or network). Secrets reach a function
47
- only through an operator grant pinned to the project revision.
46
+ heap per call, no Node, filesystem or network). The `env`/`secrets` the
47
+ runtime injects into a function come only from an operator grant pinned to
48
+ the project revision; the grant governs that injected context, not the
49
+ ambient Node environment trusted in-process code can reach on its own.
48
50
  4. **Accounts.** The `auth` extension: sign-in, registration, MFA, account
49
51
  page and protected routes. The operator installs it in a host file outside
50
52
  the project; YAML only declares the mount and configuration.
@@ -61,17 +63,19 @@ was removed from core. A `urlcode-dynamic-link` package owned them the same way
61
63
  package occupies this rung today.
62
64
 
63
65
  Rungs 1 to 3 need only the core package. Rungs 4 to 6 need the extension
64
- packages, installed from npm as `0.1.0-alpha.x` prereleases, and a Node host
65
- with a patched SQLite build; see each repository's README for the exact
66
- requirement.
66
+ packages installed from npm and a Node host with a
67
+ patched SQLite build; see each package's README ([auth](../packages/auth/README.md),
68
+ [admin](../packages/admin/README.md), [ui](../packages/ui/README.md)) for the
69
+ exact requirement.
67
70
 
68
71
  ## The composition contract
69
72
 
70
- An extended project starts with the packages and one command:
73
+ After the aligned `0.4.1` packages are published, an extended project starts
74
+ with the packages and one command:
71
75
 
72
76
  ```sh
73
- npm install @jimhoyd/urlcode @jimhoyd/urlcode-ui @jimhoyd/urlcode-auth @jimhoyd/urlcode-admin
74
- urlcode init my-site --with auth,admin
77
+ 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
78
+ urlcode init my-site --with ui,auth,admin
75
79
  ```
76
80
 
77
81
  Installing from npm is the normal path; `scripts/pack-sources.mjs` still builds
@@ -140,7 +144,7 @@ See each package README for its complete kit registration example.
140
144
  urlcode serve --project /absolute/site --host-file /absolute/operator/host.mjs --origin https://site.example
141
145
  ```
142
146
 
143
- `urlcode init <dir> --with auth,admin` writes this layout in one step: it
147
+ `urlcode init <dir> --with ui,auth,admin` writes this layout in one step: it
144
148
  resolves each installed `@jimhoyd/urlcode-<name>` from the current directory,
145
149
  calls its `scaffold` export and merges the fragments into `app/urlcode.yaml`,
146
150
  one `host.mjs` and one `README.md`, refusing before writing anything when a
@@ -151,6 +155,17 @@ administrator from JSON on stdin. `inspectExtensionRevision(project)` prints
151
155
  the SHA-256 that `projectSha256` must carry; changing extension YAML, policies
152
156
  or mounts changes the revision and needs an explicit operator reapproval.
153
157
 
158
+ The presentation tooling composes the same way, by naming packages rather than
159
+ depending on them. `urlcode-ui` is the kit alone until
160
+ `--extensions @jimhoyd/urlcode-auth,@jimhoyd/urlcode-admin` names the packages
161
+ that ship the other namespaces: each is resolved from `--project` with Node
162
+ package resolution and imported for the namespace it exports, so `list`,
163
+ `doctor`, `eject`, `preview` and `copy --missing` cover the `auth/*` and
164
+ `admin/*` templates and copy the host registers, and a project override of an
165
+ extension template is checked against the shipped view model. A package that is
166
+ not installed is skipped. `urlcode init --with` writes the commands with the
167
+ flag already set; `@jimhoyd/urlcode-ui` still depends on neither peer.
168
+
154
169
  ## Rules an agent must follow
155
170
 
156
171
  These are the facts that keep generated projects valid. The full matrix is in
@@ -195,8 +210,8 @@ These are the facts that keep generated projects valid. The full matrix is in
195
210
  | Need | Read |
196
211
  |---|---|
197
212
  | Write or change routes | [YAML guide](YAML-GUIDE.md), [field reference](YAML-REFERENCE.md), [cookbook](../examples/cookbook/README.md) |
198
- | 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) |
199
- | Add administration | [admin README](https://github.com/jimhoyd-com/urlcode-admin#readme) |
200
- | 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) |
213
+ | Add accounts | [auth README](../packages/auth/README.md), [auth security](../packages/auth/SECURITY.md) |
214
+ | Add administration | [admin README](../packages/admin/README.md) |
215
+ | Restyle every page | [ui README](../packages/ui/README.md), [ui contract](../packages/ui/CONTRACT.md) |
201
216
  | Write an extension | [extensions](EXTENSIONS.md), [extension model review](archive/2026-09-19/SPIKE-EXTENSION-MODEL.md) |
202
217
  | Run it | [operations](OPERATIONS.md), [install](INSTALL.md), [deployment checks](DEPLOYMENT-CHECKS.md) |
@@ -93,14 +93,11 @@ machinery either way.
93
93
 
94
94
  This engine — worker spawning, the module-allowlist walk, the two-layer
95
95
  deadline, `maxBytes` and response-shape validation — is one implementation
96
- shared by route dispatch and by `@jimhoyd/urlcode/sandbox`'s `SandboxPool`,
97
- the public primitive an extension package uses to run a project-supplied hook
98
- through this exact isolation when the project's own config declares
99
- `sandbox: true` on it (see [EXTENSIONS.md](EXTENSIONS.md#project-level-lifecycle-hooks)
100
- and [TYPESCRIPT.md](TYPESCRIPT.md)). Every guarantee above applies identically
101
- through that entry point; there is no separate, weaker sandbox for extensions
102
- to reach for, and no "trusted" mode exported there — trusted execution needs
103
- no primitive at all (docs/SPIKE-DEFAULT-TRUST-MODEL.md).
96
+ shared by route dispatch and by `@jimhoyd/urlcode/sandbox`'s `SandboxPool`.
97
+ That public HTTP-shaped primitive remains available to extension authors, but
98
+ project extension hooks use arbitrary typed values and contract v1 runs them
99
+ trusted in-process; it rejects `sandbox: true` rather than claiming HTTP sandbox
100
+ semantics apply to them. See [extensions](EXTENSIONS.md#project-level-lifecycle-hooks).
104
101
 
105
102
  ## What the trusted default can and can't do
106
103
 
package/docs/INSTALL.md CHANGED
@@ -8,8 +8,13 @@ so the installed `urlcode` command runs `dist/cli.js` and needs no build tool.
8
8
 
9
9
  ## npm
10
10
 
11
+ The stable release target is `0.4.1`. Use these commands after publication;
12
+ `npm run release:status` in a checkout reports live registry availability. An
13
+ unversioned npm install selects the current `latest` channel, while `@alpha`
14
+ explicitly selects the separate prerelease channel.
15
+
11
16
  ```sh
12
- npm install --global @jimhoyd/urlcode
17
+ npm install --global @jimhoyd/urlcode@0.4.1
13
18
  urlcode --help
14
19
  ```
15
20
 
@@ -19,12 +24,12 @@ Which dependency list it belongs in depends on how the project uses URLCode:
19
24
  ```sh
20
25
  # Using URLCode as a tool: validate, test and build in CI, never imported by
21
26
  # the code that serves requests.
22
- npm install --save-dev @jimhoyd/urlcode
27
+ npm install --save-dev --save-exact @jimhoyd/urlcode@0.4.1
23
28
  npx urlcode validate
24
29
 
25
30
  # Embedding the runtime (see TYPESCRIPT.md): the application imports
26
31
  # @jimhoyd/urlcode at startup, so it must survive `npm ci --omit=dev`.
27
- npm install --save @jimhoyd/urlcode
32
+ npm install --save --save-exact @jimhoyd/urlcode@0.4.1
28
33
  ```
29
34
 
30
35
  A devDependency is absent from a production install, so an application that
@@ -65,7 +70,7 @@ It downloads the release tarball, verifies its SHA-256 against the release's
65
70
  `SHA256SUMS`, and installs with npm. Options:
66
71
 
67
72
  ```sh
68
- curl -fsSL .../install.sh | sh -s -- --version 0.3.0 --prefix "$HOME/.local"
73
+ curl -fsSL .../install.sh | sh -s -- --version 0.4.1 --prefix "$HOME/.local"
69
74
  ```
70
75
 
71
76
  `--prefix` avoids needing privileges for a global npm directory; add
@@ -76,12 +81,12 @@ for that moment: to inspect first, download it, read it, then run it.
76
81
 
77
82
  No image is published yet: the release job's GHCR step is gated behind the
78
83
  `PUBLISH_CONTAINER` repository variable and has not run, so there is nothing at
79
- `ghcr.io/jimhoyd-com/urlcode` to pull. Build it from a release checkout:
84
+ `ghcr.io/jimhoyd-com/urlcode` to pull. After the release tag exists, build it from that checkout:
80
85
 
81
86
  ```sh
82
- git clone --branch v0.3.0 https://github.com/jimhoyd-com/urlcode.git
83
- docker build -t urlcode:0.3.0 urlcode
84
- docker run --rm -p 127.0.0.1:3000:3000 -v "$PWD:/project:ro" urlcode:0.3.0 \
87
+ git clone --branch v0.4.1 https://github.com/jimhoyd-com/urlcode.git
88
+ docker build -t urlcode:0.4.1 urlcode
89
+ docker run --rm -p 127.0.0.1:3000:3000 -v "$PWD:/project:ro" urlcode:0.4.1 \
85
90
  serve --project /project --host 0.0.0.0
86
91
  ```
87
92
 
@@ -48,12 +48,18 @@ Every middleware and the function share the same request and context. Validated
48
48
  with a fresh `context.state` object for this chain. State never survives the
49
49
  request. Header edits are visible downstream; editing the request does not
50
50
  reroute it or change already validated inputs. Request bodies are single-use:
51
- reading one consumes it for downstream code. There is no `clone()` or streaming
52
- API; pass parsed data through `context.state` when needed.
51
+ reading one consumes it for downstream code. On a `sandbox: true` route there is
52
+ no `clone()` or streaming API at all, so pass parsed data through
53
+ `context.state`; a trusted route receives Node's own `Request`/`Response` and so
54
+ does have `clone()`, but passing parsed data through `context.state` keeps the
55
+ chain portable between the two modes.
53
56
 
54
57
  Function responses support the existing text/JSON guest API. To transform their
55
- body, read it and return a new `Response`. Native redirect/respond/page/static/
56
- download bodies are opaque and cannot be read through `text()` or `json()`.
58
+ body, read it and return a new `Response`. On a `sandbox: true` route, native
59
+ redirect/respond/page/static/download bodies are opaque and cannot be read
60
+ through `text()` or `json()`; a trusted chain receives them as an ordinary
61
+ `Response` and can read them, so wrapping `respond: {text: hello}` and returning
62
+ `HELLO` works there and fails in the guest.
57
63
  Returning the same native response preserves original bytes, including binary
58
64
  files, ranges and HEAD lengths. You may add headers, but cannot change its
59
65
  original status or existing native headers while preserving that body. To replace