@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
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 48,160 tokens (estimate: characters / 4). -->
2
+ <!-- Consolidated URLCode authoring reference: 16 documents, about 52,118 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.
@@ -105,17 +108,19 @@ was removed from core. A `urlcode-dynamic-link` package owned them the same way
105
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
126
  Installing from npm is the normal path; `scripts/pack-sources.mjs` still builds
@@ -184,7 +189,7 @@ See each package README for its complete kit registration example.
184
189
  urlcode serve --project /absolute/site --host-file /absolute/operator/host.mjs --origin https://site.example
185
190
  ```
186
191
 
187
- `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
188
193
  resolves each installed `@jimhoyd/urlcode-<name>` from the current directory,
189
194
  calls its `scaffold` export and merges the fragments into `app/urlcode.yaml`,
190
195
  one `host.mjs` and one `README.md`, refusing before writing anything when a
@@ -195,6 +200,17 @@ administrator from JSON on stdin. `inspectExtensionRevision(project)` prints
195
200
  the SHA-256 that `projectSha256` must carry; changing extension YAML, policies
196
201
  or mounts changes the revision and needs an explicit operator reapproval.
197
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
+
198
214
  ### Rules an agent must follow
199
215
 
200
216
  These are the facts that keep generated projects valid. The full matrix is in
@@ -239,9 +255,9 @@ These are the facts that keep generated projects valid. The full matrix is in
239
255
  | Need | Read |
240
256
  |---|---|
241
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) |
242
- | 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) |
243
- | Add administration | [admin README](https://github.com/jimhoyd-com/urlcode-admin#readme) |
244
- | 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) |
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) |
245
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) |
246
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) |
247
263
 
@@ -276,7 +292,11 @@ fields or bypass target limits or operator grants. See [the design principle](ht
276
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).
277
293
  7. [The framework](https://github.com/jimhoyd-com/urlcode/blob/main/docs/FRAMEWORK.md) for accounts, administration and presentation:
278
294
  `extensions.<name>` blocks and `extension` mounts are the only YAML those
279
- 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.
280
300
 
281
301
  The root [llms.txt](https://github.com/jimhoyd-com/urlcode/blob/main/llms.txt) is a compact discovery index; the generated
282
302
  [llms-full.txt](https://github.com/jimhoyd-com/urlcode/blob/main/llms-full.txt) concatenates the authoring documents above in
@@ -520,9 +540,10 @@ either `true` or `false`) or `sandbox: true` is enough to silence it.
520
540
 
521
541
  The same judgment call applies to a project-level lifecycle hook an
522
542
  extension invokes (`onSignUp`, `beforeRegister` and the like) — it is
523
- first-party project code with the same trusted-by-default rule and the same
524
- `sandbox: true` opt-in as any `function`/`middleware` route, no special
525
- case. See [EXTENSIONS.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md#project-level-lifecycle-hooks).
543
+ first-party project code with the same trusted-by-default rule as a
544
+ `function`/`middleware` route. Extension hook contract v1 is trusted-only;
545
+ `sandbox: true` is rejected rather than silently ignored. See
546
+ [EXTENSIONS.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md#project-level-lifecycle-hooks).
526
547
 
527
548
  Guest TypeScript needs `build-typescript --project SOURCE --out NEW_DIRECTORY`
528
549
  before serving. Only the emitted `.js`/`.mjs` executes, in QuickJS for a
@@ -541,7 +562,7 @@ origin grants pinned to the project revision. These are self-hosted features;
541
562
  providers refuse them. Signals are bounded best effort with drops, no retries
542
563
  or persistence. Never turn a user request into an implicit network grant.
543
564
 
544
- Before using a feature, ask `urlcode capabilities <name>` for its constraints, grants and target support and `urlcode schema <path>` for only that YAML fragment (MCP: `get_capability`, `get_schema`), instead of guessing.
565
+ Before using a feature, ask `urlcode capabilities <name>` for its constraints, grants and target support and `urlcode schema <path>` for only that YAML fragment (MCP: `get_capability`, `get_schema`), instead of guessing. For an installed extension, use `urlcode extensions --host-file ... --json` or MCP `get_extensions`; its hook contracts include the accepted names, purpose and input/output schemas. Prefer extension configuration and UI copy/templates/theme/CSS, then a declared project hook, and only then a new extension or fork.
545
566
  The [tooling SDK and stdio MCP](https://github.com/jimhoyd-com/urlcode/blob/main/docs/TOOLING.md) inspect, validate, explain and preview
546
567
  without guest execution, environment reads or writes. Run `urlcode explain /route`
547
568
  to check effective methods, policies and cache outcome, and `urlcode manifest`
@@ -1307,7 +1328,11 @@ entry may likewise be a string, normalized to `{source: <string>}`. Only the
1307
1328
  long form exists after loading, so `routes`, `audit`, `explain`, revision hashes
1308
1329
  and the field reference describe the expansion.
1309
1330
 
1310
- ES modules only (`.mjs` or `.js`, independent of Node package settings).
1331
+ ES modules only. `.mjs` is loaded as ESM in both modes. A `sandbox: true` route
1332
+ also reads `.js` as ESM independently of Node package settings, but a trusted
1333
+ route imports `.js` through Node's own loader, so an ESM `.js` module needs the
1334
+ nearest `package.json` to declare `"type": "module"`; under `"type": "commonjs"`
1335
+ it fails activation. Prefer `.mjs` for modules that must work in either mode.
1311
1336
  [Build-time TypeScript authoring](https://github.com/jimhoyd-com/urlcode/blob/main/docs/TYPESCRIPT-AUTHORING.md) can produce these
1312
1337
  JavaScript modules in a separate output project; serving does not transpile them.
1313
1338
  The build never imports application code into Node, uses fixed compiler settings,
@@ -1744,12 +1769,18 @@ Every middleware and the function share the same request and context. Validated
1744
1769
  with a fresh `context.state` object for this chain. State never survives the
1745
1770
  request. Header edits are visible downstream; editing the request does not
1746
1771
  reroute it or change already validated inputs. Request bodies are single-use:
1747
- reading one consumes it for downstream code. There is no `clone()` or streaming
1748
- API; pass parsed data through `context.state` when needed.
1772
+ reading one consumes it for downstream code. On a `sandbox: true` route there is
1773
+ no `clone()` or streaming API at all, so pass parsed data through
1774
+ `context.state`; a trusted route receives Node's own `Request`/`Response` and so
1775
+ does have `clone()`, but passing parsed data through `context.state` keeps the
1776
+ chain portable between the two modes.
1749
1777
 
1750
1778
  Function responses support the existing text/JSON guest API. To transform their
1751
- body, read it and return a new `Response`. Native redirect/respond/page/static/
1752
- download bodies are opaque and cannot be read through `text()` or `json()`.
1779
+ body, read it and return a new `Response`. On a `sandbox: true` route, native
1780
+ redirect/respond/page/static/download bodies are opaque and cannot be read
1781
+ through `text()` or `json()`; a trusted chain receives them as an ordinary
1782
+ `Response` and can read them, so wrapping `respond: {text: hello}` and returning
1783
+ `HELLO` works there and fails in the guest.
1753
1784
  Returning the same native response preserves original bytes, including binary
1754
1785
  files, ranges and HEAD lengths. You may add headers, but cannot change its
1755
1786
  original status or existing native headers while preserving that body. To replace
@@ -2672,29 +2703,18 @@ Node adapter conformance is not a live-provider deployment claim.
2672
2703
 
2673
2704
  ### Project-level lifecycle hooks
2674
2705
 
2675
- `authorize` and `middleware` let core call *into* an extension. They do not
2676
- let a project hand its own code *to* an extension to run at a defined point.
2677
- That gap matters once an extension has meaningful lifecycle events —
2678
- registration, deletion, an administrative action, a link resolution, and so
2679
- on. Presentation already has a standard layering mechanism for this: a
2680
- project customizes an extension's *look* through `urlcode-ui`'s
2681
- `copy`/`extra.css`/`templates` without forking it. Behavior needs the same
2682
- standard, or every extension author (and every project depending on one)
2683
- either reinvents it or forks the extension. `urlcode-auth` and
2684
- `urlcode-admin` independently hit this gap (auth/#35, admin/#32); auth's own
2685
- [SPIKE-AUTH.md](https://github.com/jimhoyd-com/urlcode-auth/blob/main/docs/SPIKE-AUTH.md)
2686
- already scoped a shape for `onSignUp`/`beforeRegister`/`onDelete`.
2687
-
2688
- **The pattern.** An extension with lifecycle events an author judges worth
2689
- exposing should let the project name its own function in the extension's own
2690
- `config`, using the same source shape `function`/`middleware` routes already
2691
- use (a string path, or `{source, export, args}` — `schemas/urlcode.schema.json`),
2692
- and add its own `sandbox` boolean next to it (below). The extension's own
2693
- `activate()` reads that config, and its own runtime dispatch — not a new core
2694
- primitive, an ordinary call the extension package makes with the request
2695
- context it already has — invokes the named function at the lifecycle point
2696
- it defines, with a typed input and a typed verdict the extension's own
2697
- schema documents. For example, an auth-style extension might declare:
2706
+ Extensions expose project customization points through the core hook primitive.
2707
+ Each registration publishes `hooks`, a machine-readable list containing the
2708
+ hook name, whether it is a value-transforming `filter` or side-effect `action`,
2709
+ its description and its input/output JSON Schemas. The extension embeds
2710
+ `extensionHooksSchema(contracts)` in its configuration schema and calls
2711
+ `loadExtensionHooks(config.hooks, contracts, context)` during activation.
2712
+ Core then enforces the common source/export shape, project-root confinement,
2713
+ known names, eager module/export validation, input/output schemas and reload
2714
+ cache busting. Hook entry bytes participate in the project revision, so editing
2715
+ a hook invalidates the operator's extension pin.
2716
+
2717
+ Projects select those declared hooks in the extension's own configuration:
2698
2718
 
2699
2719
  ```yaml
2700
2720
  extensions:
@@ -2710,76 +2730,51 @@ extensions:
2710
2730
  ```
2711
2731
 
2712
2732
  with `beforeRegister` called before an account is created, given a typed
2713
- `{email, traits}`-shaped input and returning a typed verdict (`{allow: true}`
2733
+ `{email, profile?}` input and returning a typed verdict (`{allow: true}`
2714
2734
  or `{allow: false, reason}`), and `onSignUp` called after, for side effects
2715
- such as provisioning a workspace. The exact hook names, input/verdict shapes
2716
- and invocation points are the extension's own design — `hooks` is not a core
2717
- schema key — but the source/export/sandbox shape, and the fact that this is
2718
- project code the extension calls rather than an operator callback in
2719
- `host.mjs`, should be consistent across extensions so an author who has
2720
- learned one has learned the pattern.
2721
-
2722
- **Trust and execution mode.** Project-level lifecycle hooks are first-party
2723
- project code, the same category as any `function`/`middleware` route, and
2724
- follow the same trusted-by-default rule with no special case
2725
- (docs/SPIKE-DEFAULT-TRUST-MODEL.md, [FUNCTION-SECURITY.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/FUNCTION-SECURITY.md)):
2726
- trusted, in-process execution by default; a project sets `sandbox: true` on
2727
- a given hook to opt that hook into isolated QuickJS/WASM execution, exactly
2728
- the mechanism `function`/`middleware` routes already use and for the same
2729
- reason — the hook's own code, not the trustworthiness of whatever triggered
2730
- it, is what calls for isolation (docs/AI-AUTHORING.md's "Deciding when a
2731
- route needs `sandbox: true`"). This was raised as an open question — whether
2732
- a lifecycle hook should always run sandboxed because it makes a
2733
- security-relevant decision — and settled the other way: uniformity with the
2734
- rest of the trust model was chosen over hardwiring isolation for lifecycle
2735
- hooks specifically, the same "no special case" call already made between
2736
- `function` and `middleware` trust. A `beforeRegister` hook enforcing "only
2737
- `@acme.com` may register" is the project's own governance rule over its own
2738
- signup flow; it is not more dangerous than any other route the project
2739
- wrote, and does not get a different default.
2740
-
2741
- Core's own trusted/sandboxed dispatch (`TrustedFunctions`/`FunctionPool`,
2742
- `src/runtime.ts`) is wired to route dispatch, not exposed to extensions — but
2743
- each half of a hook's `sandbox: true` opt-in has its own answer:
2744
-
2745
- - **Trusted (the default, no `sandbox: true`).** No core primitive is needed
2746
- or provided: this is ordinary first-party project code, and the
2747
- extension's own `activate()` already has `ExtensionActivation.root` to
2748
- resolve the hook's `source` against and can `import()` it directly, the
2749
- same way any trusted `function`/`middleware` route does. Do that import
2750
- with a per-activation cache-busting query, the way core's own trusted
2751
- route activation does (`src/trusted-functions.ts`): Node's ESM loader
2752
- caches a resolved module forever by URL, so a plain `import()` of the
2753
- unchanged file URL makes a second activation in the same process keep
2754
- serving the hook code that was on disk at the first one
2755
- (jimhoyd-com/urlcode#198). Only the hook's **entry** module is refreshed
2756
- this way — modules the hook itself imports stay on Node's module cache,
2757
- the same limitation the trusted route path has, so a change to a hook's
2758
- own dependency still needs a process restart.
2759
- - **Sandboxed (`sandbox: true`).** `@jimhoyd/urlcode/sandbox` exports
2760
- `SandboxPool`, the same QuickJS/worker-thread engine that backs a
2761
- sandboxed `function`/`middleware` route — the identical module-allowlist
2762
- walk, memory/stack limits, two-layer deadline enforcement, `maxBytes` and
2763
- response-shape validation, with no separate or weaker engine for
2764
- extensions. It takes an explicit list of `{source, export}` entries
2765
- (resolve a hook's `source` string with the re-exported `functionFile()`,
2766
- the same resolution/validation a native route's `source` gets) instead of
2767
- anything route/YAML-shaped, and `execute({entry, chain}, request, context)`
2768
- in place of a `FunctionRoute`. There is no "trusted" mode exported
2769
- alongside it — `SandboxPool` is only ever the isolated path; see
2770
- [FUNCTION-SECURITY.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/FUNCTION-SECURITY.md) and
2771
- [TYPESCRIPT.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/TYPESCRIPT.md) for the full contract.
2772
-
2773
- An extension honoring a hook's `sandbox: true` is expected to actually
2774
- isolate that invocation through `SandboxPool` now that the primitive exists
2775
- (or document plainly that it does not yet, rather than accepting the field
2776
- and silently running it trusted) — say which, in the extension's own docs,
2777
- so an author reading them is not misled about what opt-in exists.
2735
+ such as provisioning a workspace. Hook names and lifecycle timing remain the
2736
+ extension's domain, while their declaration, loading and discovery are shared.
2737
+
2738
+ Hooks are first-party project code and run trusted in-process by default, with
2739
+ full Node access, like trusted `function` and `middleware` routes. Contract v1
2740
+ does not define an arbitrary-value sandbox hook protocol. A hook reference with
2741
+ `sandbox: true` is rejected during activation rather than silently run trusted.
2742
+ Only the entry module is refreshed during reactivation; its imported dependencies
2743
+ remain in Node's module cache until restart.
2744
+
2745
+ The UI extension exposes `transformView`, a synchronous filter called before a
2746
+ named kit template renders. It receives `{template, view}` and returns the view
2747
+ model to render. Use copy, templates, theme and CSS for ordinary presentation
2748
+ changes; use this hook for project-specific computed view data that those
2749
+ declarative layers cannot express.
2750
+
2751
+ ### Building an extension
2752
+
2753
+ An extension package should export a registration factory and, when it supports
2754
+ `urlcode init --with`, a side-effect-free `scaffold` function. The registration:
2755
+
2756
+ 1. Declares its logical name, contract version, supported targets, exact project
2757
+ revision pin and strict configuration/policy schemas.
2758
+ 2. Publishes every project hook through `hooks` and reuses
2759
+ `extensionHooksSchema` plus `loadExtensionHooks`; it does not implement its
2760
+ own path resolver or dynamic-import cache.
2761
+ 3. Activates all configuration, files, services and hooks before serving a
2762
+ request. Invalid or stale configuration fails activation.
2763
+ 4. Returns `handle` for mounts and optionally `authorize`/`middleware` for route
2764
+ policies. It closes resources it owns.
2765
+ 5. Keeps credentials, storage and provider setup in the operator host. Project
2766
+ YAML contains logical configuration and project-relative hook references.
2767
+
2768
+ Consumers install the package, declare its YAML block and mounts/policies, and
2769
+ register it in `host.mjs`. They modify it through declared configuration,
2770
+ presentation layers and hooks. A fork is reserved for changing behavior the
2771
+ extension has not exposed; that is evidence for a new declarative field or hook.
2772
+ See [Composing a site](https://github.com/jimhoyd-com/urlcode/blob/main/docs/COMPOSING-A-SITE.md) for the complete ui/auth/admin example.
2778
2773
 
2779
2774
  ### Discovering schemas
2780
2775
 
2781
2776
  Each registration carries the JSON Schemas that validate its `config` block and
2782
- its per-route policy requirements. `urlcode extensions` prints them together with
2777
+ its per-route policy requirements, plus its hook contracts. `urlcode extensions` prints them together with
2783
2778
  the project's own declarations so an author can see what a mount accepts:
2784
2779
 
2785
2780
  ```sh
@@ -2788,6 +2783,7 @@ urlcode extensions --project ./site --host-file /absolute/operator/host.mjs [--j
2788
2783
 
2789
2784
  For every registration in the host file it reports the name, contract version,
2790
2785
  targets, credential headers, configuration schema, policy schema (if any),
2786
+ declared hook names, kinds, descriptions and input/output schemas,
2791
2787
  whether the project declares it, whether its `projectSha256` matches the current
2792
2788
  revision, the routes that mount it and the routes whose policies require it.
2793
2789
  Declared names the host does not register are listed as unregistered. The command
@@ -2825,16 +2821,15 @@ boundary, not a JavaScript sandbox or an independent security review.
2825
2821
 
2826
2822
  ### Scaffolding with `init --with`
2827
2823
 
2828
- `urlcode init <directory> --with auth,admin` produces the layered site the
2824
+ `urlcode init <directory> --with ui,auth,admin` produces the layered site the
2829
2825
  [framework page](https://github.com/jimhoyd-com/urlcode/blob/main/docs/FRAMEWORK.md#the-composition-contract) describes in one
2830
2826
  command: the starter under `<directory>/app/`, one `host.mjs`, one `README.md`,
2831
2827
  and each extension's own operator files. Core never bundles or imports the
2832
2828
  extension packages at build time; at run time it resolves
2833
2829
  `@jimhoyd/urlcode-<name>` for each name with Node's package resolution from
2834
- the invoking directory (so `npm install @jimhoyd/urlcode-auth` in that
2835
- directory, from npm where the packages are published as `0.1.0-alpha.x`
2836
- prereleases, is the normal path and what makes `--with auth` work), imports
2837
- the package and calls its
2830
+ the invoking directory. Install a compatible package set there, as shown in
2831
+ [the framework guide](https://github.com/jimhoyd-com/urlcode/blob/main/docs/FRAMEWORK.md#the-composition-contract), before using
2832
+ `--with ui,auth`; UI must activate before auth. Core imports the package and calls its
2838
2833
  `scaffold` export with this request:
2839
2834
 
2840
2835
  ```ts
@@ -2890,6 +2885,37 @@ Assembly rules, in `--with` order:
2890
2885
  review the project and pin it explicitly; the host is generated to require
2891
2886
  the pin, never to compute it.
2892
2887
 
2888
+ #### Recorded versions
2889
+
2890
+ `init --with` also writes `<directory>/package.json`: a private manifest that
2891
+ pins, at exactly the version that was resolved, the running runtime, every
2892
+ extension named in `--with`, and every package those extensions declare in
2893
+ `peerDependencies` (so `@jimhoyd/urlcode-ui` is pinned for an `auth,admin` site
2894
+ although nobody named it). Before anything is written, the whole set is checked
2895
+ against every declared peer range; an incompatible combination or a missing
2896
+ required peer refuses and names it, leaving no directory behind.
2897
+
2898
+ Nothing is installed. The generated site has no `node_modules` and no
2899
+ `package-lock.json` until you run `npm install` in it yourself, which the
2900
+ command and the generated README both state as the next step. Reproducibility
2901
+ comes from that install, not from generation.
2902
+
2903
+ - `--no-manifest` generates the site without a `package.json`, for a site whose
2904
+ dependencies are managed elsewhere. Plain `urlcode init` is unchanged and
2905
+ still writes no manifest; add `--manifest` to pin the runtime for a
2906
+ route-only project too.
2907
+ - `--pin <package>=<specifier>` records a specifier instead of the resolved
2908
+ version, for local tarball or offline development
2909
+ (`--pin @jimhoyd/urlcode-auth=file:/abs/urlcode-auth-0.1.0-alpha.6.tgz`). A
2910
+ package installed from a local path or tarball is detected from npm's own
2911
+ install record and pinned by that path without any flag; the README says so,
2912
+ because such a pin only reproduces where that path exists.
2913
+
2914
+ There is no upgrade command. Moving a generated project to newer versions today
2915
+ means editing its `package.json` and re-running `npm install` yourself; nothing
2916
+ in this runtime selects a newer tested set, shows the change, or updates a
2917
+ lockfile for you.
2918
+
2893
2919
  Serving the result is the usual explicit host binding:
2894
2920
 
2895
2921
  ```sh
@@ -2898,6 +2924,298 @@ urlcode validate --project app --host-file "$PWD/host.mjs" --origin https://site
2898
2924
 
2899
2925
  ---
2900
2926
 
2927
+ # Composing a site from ui, auth and admin
2928
+
2929
+ Source: https://github.com/jimhoyd-com/urlcode/blob/main/docs/COMPOSING-A-SITE.md
2930
+
2931
+ One command produces a site that already has accounts, an administration
2932
+ console and a presentation kit wired together:
2933
+
2934
+ ```sh
2935
+ npm install @jimhoyd/urlcode @jimhoyd/urlcode-ui @jimhoyd/urlcode-auth @jimhoyd/urlcode-admin
2936
+ urlcode init site --with ui,auth,admin
2937
+ ```
2938
+
2939
+ This page is the map of what you may then change, and with which tool. It
2940
+ covers three different activities that are easy to confuse:
2941
+
2942
+ | You want to | Use | Code? |
2943
+ |---|---|---|
2944
+ | Change what an extension is configured to do | the `config` block in `app/urlcode.yaml` | none |
2945
+ | Change how its screens look or read | files under `<site>/ui/` | none (templates are data) |
2946
+ | Run your own logic at one of its lifecycle points | a project function named from that `config` | first-party project JavaScript |
2947
+ | Add a capability none of the three provides | a new extension package | TypeScript against the runtime contract |
2948
+
2949
+ Work down that list, not up. The [declarative-first
2950
+ principle](https://github.com/jimhoyd-com/urlcode/blob/main/docs/PROJECT-DIRECTION.md#design-principle-declarative-first) applies
2951
+ here as much as it does to routes: reach for the next row only when the one
2952
+ above it cannot express the requirement.
2953
+
2954
+ ### What `--with ui,auth,admin` generates
2955
+
2956
+ - `site/app/` — the route project: `urlcode.yaml` with an `extensions` block
2957
+ per package, and `routes/extensions.yaml` holding `/assets/ui/*`,
2958
+ `/account/*`, `/private` and `/admin/*`.
2959
+ - `site/host.mjs` — the operator host module, the one place that holds code.
2960
+ It builds the kit with `createUiExtension`, passes the returned object into
2961
+ `authExtension` and `adminExtension`, and lists `ui.registration` first.
2962
+ - `site/ui/` — `copy/`, `templates/` and `extra.css`, the project's
2963
+ presentation overrides, beside the host and **outside** `app/`.
2964
+ - `site/operator-service.mjs`, `site/data/` — auth's operator service and its
2965
+ private key material, mode `0600`.
2966
+ - `site/README.md` — the merged next steps, environment table and the project
2967
+ revision to review and pin.
2968
+
2969
+ Nothing about that wiring is manual any more. The generated host registers
2970
+ `authCatalogue` as a copy source and both `authUiTemplates` and
2971
+ `adminUiTemplates` as template namespaces, because auth and admin render only
2972
+ through the kit and refuse to activate without their own templates present.
2973
+
2974
+ #### Supported combinations
2975
+
2976
+ `--with` order is the activation order, and the kit must be active before
2977
+ anything that renders through it.
2978
+
2979
+ | `--with` | Result |
2980
+ |---|---|
2981
+ | `ui` | Kit only; the host wires no peer catalogue or templates. |
2982
+ | `ui,auth` | Accounts on `/account/*`, rendered through the kit. |
2983
+ | `ui,auth,admin` | The full composition above. |
2984
+ | `auth` or `auth,admin` | Refused: the scaffold names the missing `ui`. |
2985
+ | `auth,admin,ui` | Refused: `ui` must come before the extensions it renders. |
2986
+ | `admin` without `auth` | Refused: admin reuses auth's service, CSRF key and revision. |
2987
+
2988
+ Every refusal happens before anything is written, and leaves no directory
2989
+ behind. There is no auth-without-ui or admin-without-ui configuration in this
2990
+ revision: the UI primitive fallback was retired, so the kit is the only render
2991
+ path (see [OPEN-DECISIONS.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/OPEN-DECISIONS.md)).
2992
+
2993
+ ### Declarative configuration
2994
+
2995
+ Each package owns one `extensions.<name>` block. The block itself is core
2996
+ schema (`version` plus `config`); what may go inside `config` is the
2997
+ package's own JSON Schema, which you can print rather than guess:
2998
+
2999
+ ```sh
3000
+ urlcode extensions --project ./site/app --host-file "$PWD/site/host.mjs" --json
3001
+ ```
3002
+
3003
+ The same report is the MCP tool `get_extensions`, and it is the authoritative
3004
+ answer for both the configuration schema and the per-route policy schema. The
3005
+ generated site starts from something like this:
3006
+
3007
+ ```yaml
3008
+ version: "1"
3009
+ extensions:
3010
+ ui:
3011
+ version: "1"
3012
+ config:
3013
+ theme:
3014
+ name: My Site
3015
+ languages: [en]
3016
+ copy: ui/copy
3017
+ templates: ui/templates
3018
+ stylesheet: ui/extra.css
3019
+ auth:
3020
+ version: "1"
3021
+ config:
3022
+ registration: "off"
3023
+ admin:
3024
+ version: "1"
3025
+ config: {}
3026
+ ```
3027
+
3028
+ Routes mount an extension, and policies require one:
3029
+
3030
+ ```yaml
3031
+ routes:
3032
+ /assets/ui/*:
3033
+ extension: ui
3034
+ methods: [GET, HEAD]
3035
+ /account/*:
3036
+ extension: auth
3037
+ methods: [GET, HEAD, POST]
3038
+ /admin/*:
3039
+ extension: admin
3040
+ methods: [GET, HEAD, POST]
3041
+ /private:
3042
+ respond:
3043
+ text: Signed in
3044
+ policies:
3045
+ extensions:
3046
+ auth: {}
3047
+ ```
3048
+
3049
+ See [EXTENSIONS.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md) for the `auth` route short form, extension
3050
+ middleware, and the host-file trust boundary.
3051
+
3052
+ ### Presentation overrides
3053
+
3054
+ The `ui` config's `copy`, `templates` and `stylesheet` paths point at the
3055
+ project's own directories. Nothing here forks a package.
3056
+
3057
+ | Override | File | Effect |
3058
+ |---|---|---|
3059
+ | Wording and translation | `ui/copy/<locale>.json` | Replaces catalogue ids, including ids the auth and admin packages own. Listed in `languages`. |
3060
+ | 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`. |
3061
+ | Styling | `ui/extra.css` | Appended after the kit stylesheet; `{file, replace: true}` replaces it instead. |
3062
+ | Colours, logo, favicon, radius, font | the `theme` block | Declarative; no file needed. |
3063
+
3064
+ A template is data in the kit's own language. It cannot add a script, change
3065
+ what a form validates, or change what a page sends in headers — so an override
3066
+ cannot weaken the screen it restyles. Stylesheets containing `@import`,
3067
+ `script`, `javascript:` or `expression(` are refused.
3068
+
3069
+ Names, coverage and what the runtime will actually load:
3070
+
3071
+ ```sh
3072
+ npx urlcode-ui list --project ./site --extensions @jimhoyd/urlcode-auth,@jimhoyd/urlcode-admin
3073
+ npx urlcode-ui doctor --project ./site --extensions @jimhoyd/urlcode-auth,@jimhoyd/urlcode-admin --copy ui/copy --templates ui/templates --stylesheet ui/extra.css
3074
+ npx urlcode-ui eject auth/sign-in --out ./site/ui/templates --project ./site --extensions @jimhoyd/urlcode-auth
3075
+ npx urlcode-ui copy --missing fr --project ./site --extensions @jimhoyd/urlcode-auth --copy ui/copy --languages en,fr
3076
+ ```
3077
+
3078
+ `eject` copies the shipped source so an override starts from what ships and
3079
+ never overwrites an existing file. `ui/` lives outside `app/`, so editing copy
3080
+ or templates does **not** change the project revision and does not require
3081
+ re-pinning `PROJECT_SHA256`.
3082
+
3083
+ **Name the packages that ship the other namespaces.** `urlcode-ui` is this
3084
+ kit alone until `--extensions` names them. Each package is resolved from
3085
+ `--project` with Node package resolution and imported for the namespace it
3086
+ exports; one that is not installed there is skipped with a note, so the
3087
+ command still runs. The site's `host.mjs` is never read: it builds services
3088
+ and reads secrets at its top level, and a read-only `list` or `doctor` must
3089
+ not run it. With the packages named:
3090
+
3091
+ - `list` shows the `auth/*` and `admin/*` names beside the kit's own, each
3092
+ with its origin, and `eject auth/sign-in` copies the shipped source.
3093
+ - `doctor` reports an `expected` view model for an extension template, so its
3094
+ `behind` flag tells you when an override of one has fallen behind what
3095
+ ships. Its `extensions` field names the namespaces the report covers, so a
3096
+ report built without a peer is visible as such.
3097
+ - `preview auth/sign-in` renders the extension's own sample view model.
3098
+ - `copy --missing` skeletons cover the auth ids the account screens use.
3099
+ Admin-owned `adminUi.*` ids are deliberately not offered: admin composes
3100
+ its catalogue onto the kit's presentation rather than registering it there,
3101
+ and those translations do not currently reach the console
3102
+ ([#227](https://github.com/jimhoyd-com/urlcode/issues/227)).
3103
+
3104
+ `urlcode init <directory> --with ui,auth,admin` writes these commands into the
3105
+ generated README with the flag already set. `@jimhoyd/urlcode-ui` depends on
3106
+ neither peer; the operator names them.
3107
+
3108
+ Overrides of extension templates and of extension-owned catalogue ids reach
3109
+ the rendered screens, which is what the regression test below asserts.
3110
+
3111
+ ### Project functions: lifecycle hooks
3112
+
3113
+ A hook is your own function, named from the extension's `config`, that the
3114
+ extension calls at a lifecycle point it defines. It uses the same source shape
3115
+ a `function` route uses — a bare path, or `{source, export}` — resolved
3116
+ relative to the route project.
3117
+
3118
+ ```yaml
3119
+ extensions:
3120
+ auth:
3121
+ version: "1"
3122
+ config:
3123
+ registration: open
3124
+ hooks:
3125
+ beforeRegister:
3126
+ source: ./hooks/registration-rule.mjs
3127
+ export: default
3128
+ onSignUp: ./hooks/on-signup.mjs
3129
+ ```
3130
+
3131
+ `hooks` remains each package's own config, but core supplies the reference
3132
+ schema, trusted loader and machine-readable hook contract. `get_extensions`
3133
+ reports those contracts, so an agent can discover accepted names, purpose and
3134
+ input/output shapes without guessing from prose.
3135
+
3136
+ Hooks are first-party project code and run **trusted and in-process**, the
3137
+ same default `function` and `middleware` routes have
3138
+ ([FUNCTION-SECURITY.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/FUNCTION-SECURITY.md)). Extension hook contract v1 is
3139
+ trusted-only; `sandbox: true` is rejected loudly at activation.
3140
+
3141
+ #### `@jimhoyd/urlcode-ui`
3142
+
3143
+ | Hook | Input | Returns | Called |
3144
+ |---|---|---|---|
3145
+ | `transformView` | `{template, view}` | the view object to render | Synchronously before each public `ui.kit.render()` or `ui.kit.page()` call. Use it only when theme, copy, templates and CSS cannot express the change. |
3146
+
3147
+ For example, `transformView: ./hooks/ui-view.mjs` can add project-computed
3148
+ navigation or labels to an auth/admin view without editing either package.
3149
+
3150
+ #### `@jimhoyd/urlcode-auth`
3151
+
3152
+ | Hook | Input | Returns | Called |
3153
+ |---|---|---|---|
3154
+ | `beforeRegister` | `{email, profile?}` | `{allow: boolean, reason?}` | Before an account is created, on `POST /account/register` and on `POST /account/signup/begin`. |
3155
+ | `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. |
3156
+ | `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. |
3157
+
3158
+ #### `@jimhoyd/urlcode-admin`
3159
+
3160
+ | Hook | Input | Returns | Called |
3161
+ |---|---|---|---|
3162
+ | `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. |
3163
+ | `onRegistrationApproved` | `{requestId, accountId, email, actorId, reason}` | ignored | After a registration request is approved. |
3164
+ | `onAccountStatusChanged` | `{accountId, status, actorId, reason}` | ignored | After an account is locked or unlocked. |
3165
+
3166
+ #### Verdicts and failure
3167
+
3168
+ - **A veto is explicit.** A pre-action hook allows only by returning
3169
+ `allow: true`. `allow: false`, or no verdict at all, rejects the operation
3170
+ with `403` and the hook's own `reason`, or a generic message when it gave
3171
+ none. Nothing is written. A hook that *throws* has not returned a verdict:
3172
+ the operation is still refused, but as a generic `500`, so return a verdict
3173
+ rather than throwing when you mean to deny.
3174
+ - **Broken hooks fail at activation, not at the first request.** A missing
3175
+ module, a source path escaping the project, an export that is not a
3176
+ function, or `sandbox: true` all throw while the extension activates, naming
3177
+ the hook. The site does not start.
3178
+ - **A post-action hook cannot undo anything.** `onSignUp`,
3179
+ `onDelete`, `onRegistrationApproved` and `onAccountStatusChanged` run after
3180
+ the operation has committed. Throwing from one replaces the success response
3181
+ with a `500` while the account, approval or status change stands. There is
3182
+ no retry and no rollback. Keep them non-throwing: catch your own errors and
3183
+ queue the work instead of failing the request.
3184
+ - **A hook's message is not a channel to the browser.** Only a pre-action
3185
+ `reason` is shown. An uncaught error surfaces as a generic failure.
3186
+ - **An edited hook needs a restart.** Activation re-imports the hook's entry
3187
+ module, so a reload picks up an edit to that file — but modules it imports
3188
+ stay on Node's module cache, exactly as for trusted route functions.
3189
+
3190
+ ### TypeScript: implementing a new extension
3191
+
3192
+ Only write an extension when a capability is genuinely absent — not to
3193
+ customize one of the three above. An extension is an operator-installed
3194
+ package whose host object core activates; it is named in `host.mjs`, never
3195
+ in YAML. The contract, the activation inputs, `ExtensionActivation.root`,
3196
+ credential headers and the `projectSha256` pin are in
3197
+ [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
3198
+ `scaffold` export that makes a package work with `init --with` is in
3199
+ [EXTENSIONS.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md#scaffolding-with-init---with).
3200
+
3201
+ If its screens should be themeable the same way auth's and admin's are, it
3202
+ also exports a template namespace (and, if it ships English wording, a
3203
+ catalogue) for a host to pass to `createUiExtension`. That is what makes
3204
+ `ui/templates/<yourname>/<screen>.html` work in a consumer project without a
3205
+ fork.
3206
+
3207
+ ### What this page does not claim
3208
+
3209
+ The composition, the refusals and the override path are exercised by
3210
+ `test/workspace-scaffold.integration.ts`, which runs `init --with` against the
3211
+ built packages, drops a template and a copy catalogue into the generated
3212
+ `ui/` directory and asserts both reach a rendered auth screen and a rendered
3213
+ admin screen. That runs in-process against the generated host: no HTTP
3214
+ listener, TLS proxy, browser or deployed site is exercised, and no published
3215
+ npm tarball is checked against this checkout.
3216
+
3217
+ ---
3218
+
2901
3219
  # Function execution: trusted by default, sandboxed opt-in
2902
3220
 
2903
3221
  Source: https://github.com/jimhoyd-com/urlcode/blob/main/docs/FUNCTION-SECURITY.md
@@ -2995,14 +3313,11 @@ machinery either way.
2995
3313
 
2996
3314
  This engine — worker spawning, the module-allowlist walk, the two-layer
2997
3315
  deadline, `maxBytes` and response-shape validation — is one implementation
2998
- shared by route dispatch and by `@jimhoyd/urlcode/sandbox`'s `SandboxPool`,
2999
- the public primitive an extension package uses to run a project-supplied hook
3000
- through this exact isolation when the project's own config declares
3001
- `sandbox: true` on it (see [EXTENSIONS.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md#project-level-lifecycle-hooks)
3002
- and [TYPESCRIPT.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/TYPESCRIPT.md)). Every guarantee above applies identically
3003
- through that entry point; there is no separate, weaker sandbox for extensions
3004
- to reach for, and no "trusted" mode exported there — trusted execution needs
3005
- no primitive at all (docs/SPIKE-DEFAULT-TRUST-MODEL.md).
3316
+ shared by route dispatch and by `@jimhoyd/urlcode/sandbox`'s `SandboxPool`.
3317
+ That public HTTP-shaped primitive remains available to extension authors, but
3318
+ project extension hooks use arbitrary typed values and contract v1 runs them
3319
+ trusted in-process; it rejects `sandbox: true` rather than claiming HTTP sandbox
3320
+ semantics apply to them. See [extensions](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md#project-level-lifecycle-hooks).
3006
3321
 
3007
3322
  ### What the trusted default can and can't do
3008
3323