@jimhoyd/urlcode 0.4.0-alpha.3 → 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 (65) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +20 -15
  3. package/ROADMAP.md +16 -11
  4. package/dist/BUILD-MANIFEST.json +16 -15
  5. package/dist/authoring.js +15 -1
  6. package/dist/capability-query.js +0 -1
  7. package/dist/catalog.js +0 -1
  8. package/dist/cli.js +23 -7
  9. package/dist/config.js +1 -1
  10. package/dist/explain.js +1 -1
  11. package/dist/http-response.js +1 -1
  12. package/dist/index.js +1 -0
  13. package/dist/init-with.js +36 -11
  14. package/dist/manifest.js +1 -1
  15. package/dist/mcp-authoring.js +2 -2
  16. package/dist/mcp.js +1 -1
  17. package/dist/policies/cache.js +2 -2
  18. package/dist/project-dependencies.js +305 -0
  19. package/dist/runtime.js +1 -1
  20. package/dist/trusted-functions.js +4 -5
  21. package/dist/types/authoring.d.ts +9 -1
  22. package/dist/types/capability-query.d.ts +0 -1
  23. package/dist/types/catalog.d.ts +0 -4
  24. package/dist/types/config.d.ts +1 -9
  25. package/dist/types/explain.d.ts +0 -1
  26. package/dist/types/http-response.d.ts +0 -1
  27. package/dist/types/index.d.ts +1 -0
  28. package/dist/types/init-with.d.ts +7 -13
  29. package/dist/types/manifest.d.ts +0 -1
  30. package/dist/types/project-dependencies.d.ts +78 -0
  31. package/dist/types/trusted-functions.d.ts +1 -4
  32. package/docs/AI-AUTHORING.md +5 -1
  33. package/docs/AWS.md +9 -0
  34. package/docs/CI-FOLLOWUP-2026-09-19.md +1 -1
  35. package/docs/CODEBASE-AUDIT-2026-09-20.md +6 -0
  36. package/docs/COMPOSING-A-SITE.md +278 -0
  37. package/docs/DEVELOPMENT-PIPELINE.md +208 -119
  38. package/docs/EXTENSIONS.md +36 -6
  39. package/docs/FRAMEWORK.md +45 -30
  40. package/docs/INSTALL.md +13 -8
  41. package/docs/MIDDLEWARE.md +10 -4
  42. package/docs/OPEN-DECISIONS.md +46 -6
  43. package/docs/READINESS.md +4 -3
  44. package/docs/README.md +3 -4
  45. package/docs/RELEASE-0.4.1.md +73 -0
  46. package/docs/RELEASE-SECURITY.md +27 -12
  47. package/docs/SPECIFICATION.md +5 -1
  48. package/docs/SPIKE-CORE-LAYERING.md +1 -1
  49. package/docs/STARTERS.md +17 -5
  50. package/docs/TOOLING.md +6 -4
  51. package/docs/VERCEL.md +10 -2
  52. package/docs/VERSION-ALIGNMENT.md +42 -8
  53. package/docs/archive/2026-09-19/ROADMAP.md +1 -0
  54. package/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md +1 -0
  55. package/docs/{SPIKE-LAMBDA-COMPILE.md → archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md} +168 -12
  56. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +2 -0
  57. package/docs/archive/README.md +1 -0
  58. package/docs/yaml/functions.md +10 -2
  59. package/docs/yaml/middleware.md +5 -3
  60. package/examples/cookbook/middleware/envelope.mjs +4 -2
  61. package/llms-full.txt +387 -44
  62. package/llms.txt +1 -0
  63. package/package.json +8 -5
  64. package/packaging/claude-plugin/.claude-plugin/plugin.json +1 -1
  65. package/recipes/middleware/middleware/envelope.mjs +4 -2
@@ -0,0 +1,278 @@
1
+ # Composing a site from ui, auth and admin
2
+
3
+ One command produces a site that already has accounts, an administration
4
+ console and a presentation kit wired together:
5
+
6
+ ```sh
7
+ npm install @jimhoyd/urlcode @jimhoyd/urlcode-ui @jimhoyd/urlcode-auth @jimhoyd/urlcode-admin
8
+ urlcode init site --with ui,auth,admin
9
+ ```
10
+
11
+ This page is the map of what you may then change, and with which tool. It
12
+ covers three different activities that are easy to confuse:
13
+
14
+ | You want to | Use | Code? |
15
+ |---|---|---|
16
+ | Change what an extension is configured to do | the `config` block in `app/urlcode.yaml` | none |
17
+ | Change how its screens look or read | files under `<site>/ui/` | none (templates are data) |
18
+ | Run your own logic at one of its lifecycle points | a project function named from that `config` | first-party project JavaScript |
19
+ | Add a capability none of the three provides | a new extension package | TypeScript against the runtime contract |
20
+
21
+ Work down that list, not up. The [declarative-first
22
+ principle](PROJECT-DIRECTION.md#design-principle-declarative-first) applies
23
+ here as much as it does to routes: reach for the next row only when the one
24
+ above it cannot express the requirement.
25
+
26
+ ## What `--with ui,auth,admin` generates
27
+
28
+ - `site/app/` — the route project: `urlcode.yaml` with an `extensions` block
29
+ per package, and `routes/extensions.yaml` holding `/assets/ui/*`,
30
+ `/account/*`, `/private` and `/admin/*`.
31
+ - `site/host.mjs` — the operator host module, the one place that holds code.
32
+ It builds the kit with `createUiExtension`, passes the returned object into
33
+ `authExtension` and `adminExtension`, and lists `ui.registration` first.
34
+ - `site/ui/` — `copy/`, `templates/` and `extra.css`, the project's
35
+ presentation overrides, beside the host and **outside** `app/`.
36
+ - `site/operator-service.mjs`, `site/data/` — auth's operator service and its
37
+ private key material, mode `0600`.
38
+ - `site/README.md` — the merged next steps, environment table and the project
39
+ revision to review and pin.
40
+
41
+ Nothing about that wiring is manual any more. The generated host registers
42
+ `authCatalogue` as a copy source and both `authUiTemplates` and
43
+ `adminUiTemplates` as template namespaces, because auth and admin render only
44
+ through the kit and refuse to activate without their own templates present.
45
+
46
+ ### Supported combinations
47
+
48
+ `--with` order is the activation order, and the kit must be active before
49
+ anything that renders through it.
50
+
51
+ | `--with` | Result |
52
+ |---|---|
53
+ | `ui` | Kit only; the host wires no peer catalogue or templates. |
54
+ | `ui,auth` | Accounts on `/account/*`, rendered through the kit. |
55
+ | `ui,auth,admin` | The full composition above. |
56
+ | `auth` or `auth,admin` | Refused: the scaffold names the missing `ui`. |
57
+ | `auth,admin,ui` | Refused: `ui` must come before the extensions it renders. |
58
+ | `admin` without `auth` | Refused: admin reuses auth's service, CSRF key and revision. |
59
+
60
+ Every refusal happens before anything is written, and leaves no directory
61
+ behind. There is no auth-without-ui or admin-without-ui configuration in this
62
+ revision: the UI primitive fallback was retired, so the kit is the only render
63
+ path (see [OPEN-DECISIONS.md](OPEN-DECISIONS.md)).
64
+
65
+ ## Declarative configuration
66
+
67
+ Each package owns one `extensions.<name>` block. The block itself is core
68
+ schema (`version` plus `config`); what may go inside `config` is the
69
+ package's own JSON Schema, which you can print rather than guess:
70
+
71
+ ```sh
72
+ urlcode extensions --project ./site/app --host-file "$PWD/site/host.mjs" --json
73
+ ```
74
+
75
+ The same report is the MCP tool `get_extensions`, and it is the authoritative
76
+ answer for both the configuration schema and the per-route policy schema. The
77
+ generated site starts from something like this:
78
+
79
+ ```yaml
80
+ version: "1"
81
+ extensions:
82
+ ui:
83
+ version: "1"
84
+ config:
85
+ theme:
86
+ name: My Site
87
+ languages: [en]
88
+ copy: ui/copy
89
+ templates: ui/templates
90
+ stylesheet: ui/extra.css
91
+ auth:
92
+ version: "1"
93
+ config:
94
+ registration: "off"
95
+ admin:
96
+ version: "1"
97
+ config: {}
98
+ ```
99
+
100
+ Routes mount an extension, and policies require one:
101
+
102
+ ```yaml
103
+ routes:
104
+ /assets/ui/*:
105
+ extension: ui
106
+ methods: [GET, HEAD]
107
+ /account/*:
108
+ extension: auth
109
+ methods: [GET, HEAD, POST]
110
+ /admin/*:
111
+ extension: admin
112
+ methods: [GET, HEAD, POST]
113
+ /private:
114
+ respond:
115
+ text: Signed in
116
+ policies:
117
+ extensions:
118
+ auth: {}
119
+ ```
120
+
121
+ See [EXTENSIONS.md](EXTENSIONS.md) for the `auth` route short form, extension
122
+ middleware, and the host-file trust boundary.
123
+
124
+ ## Presentation overrides
125
+
126
+ The `ui` config's `copy`, `templates` and `stylesheet` paths point at the
127
+ project's own directories. Nothing here forks a package.
128
+
129
+ | Override | File | Effect |
130
+ |---|---|---|
131
+ | Wording and translation | `ui/copy/<locale>.json` | Replaces catalogue ids, including ids the auth and admin packages own. Listed in `languages`. |
132
+ | 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`. |
133
+ | Styling | `ui/extra.css` | Appended after the kit stylesheet; `{file, replace: true}` replaces it instead. |
134
+ | Colours, logo, favicon, radius, font | the `theme` block | Declarative; no file needed. |
135
+
136
+ A template is data in the kit's own language. It cannot add a script, change
137
+ what a form validates, or change what a page sends in headers — so an override
138
+ cannot weaken the screen it restyles. Stylesheets containing `@import`,
139
+ `script`, `javascript:` or `expression(` are refused.
140
+
141
+ Names, coverage and what the runtime will actually load:
142
+
143
+ ```sh
144
+ npx urlcode-ui list --project ./site --extensions @jimhoyd/urlcode-auth,@jimhoyd/urlcode-admin
145
+ npx urlcode-ui doctor --project ./site --extensions @jimhoyd/urlcode-auth,@jimhoyd/urlcode-admin --copy ui/copy --templates ui/templates --stylesheet ui/extra.css
146
+ npx urlcode-ui eject auth/sign-in --out ./site/ui/templates --project ./site --extensions @jimhoyd/urlcode-auth
147
+ npx urlcode-ui copy --missing fr --project ./site --extensions @jimhoyd/urlcode-auth --copy ui/copy --languages en,fr
148
+ ```
149
+
150
+ `eject` copies the shipped source so an override starts from what ships and
151
+ never overwrites an existing file. `ui/` lives outside `app/`, so editing copy
152
+ or templates does **not** change the project revision and does not require
153
+ re-pinning `PROJECT_SHA256`.
154
+
155
+ **Name the packages that ship the other namespaces.** `urlcode-ui` is this
156
+ kit alone until `--extensions` names them. Each package is resolved from
157
+ `--project` with Node package resolution and imported for the namespace it
158
+ exports; one that is not installed there is skipped with a note, so the
159
+ command still runs. The site's `host.mjs` is never read: it builds services
160
+ and reads secrets at its top level, and a read-only `list` or `doctor` must
161
+ not run it. With the packages named:
162
+
163
+ - `list` shows the `auth/*` and `admin/*` names beside the kit's own, each
164
+ with its origin, and `eject auth/sign-in` copies the shipped source.
165
+ - `doctor` reports an `expected` view model for an extension template, so its
166
+ `behind` flag tells you when an override of one has fallen behind what
167
+ ships. Its `extensions` field names the namespaces the report covers, so a
168
+ report built without a peer is visible as such.
169
+ - `preview auth/sign-in` renders the extension's own sample view model.
170
+ - `copy --missing` skeletons cover the auth ids the account screens use.
171
+ Admin-owned `adminUi.*` ids are deliberately not offered: admin composes
172
+ its catalogue onto the kit's presentation rather than registering it there,
173
+ and those translations do not currently reach the console
174
+ ([#227](https://github.com/jimhoyd-com/urlcode/issues/227)).
175
+
176
+ `urlcode init <directory> --with ui,auth,admin` writes these commands into the
177
+ generated README with the flag already set. `@jimhoyd/urlcode-ui` depends on
178
+ neither peer; the operator names them.
179
+
180
+ Overrides of extension templates and of extension-owned catalogue ids reach
181
+ the rendered screens, which is what the regression test below asserts.
182
+
183
+ ## Project functions: lifecycle hooks
184
+
185
+ A hook is your own function, named from the extension's `config`, that the
186
+ extension calls at a lifecycle point it defines. It uses the same source shape
187
+ a `function` route uses — a bare path, or `{source, export}` — resolved
188
+ relative to the route project.
189
+
190
+ ```yaml
191
+ extensions:
192
+ auth:
193
+ version: "1"
194
+ config:
195
+ registration: open
196
+ hooks:
197
+ beforeRegister:
198
+ source: ./hooks/registration-rule.mjs
199
+ export: default
200
+ onSignUp: ./hooks/on-signup.mjs
201
+ ```
202
+
203
+ `hooks` is not a core schema key. It is each package's own config, validated
204
+ by that package's schema before `activate()`, which is why `get_extensions` is
205
+ the place to check what a given version accepts.
206
+
207
+ Hooks are first-party project code and run **trusted and in-process**, the
208
+ same default `function` and `middleware` routes have
209
+ ([FUNCTION-SECURITY.md](FUNCTION-SECURITY.md)). Neither package implements
210
+ sandboxed hook execution yet; `sandbox: true` on a hook is rejected loudly at
211
+ activation rather than accepted and silently run trusted.
212
+
213
+ ### `@jimhoyd/urlcode-auth`
214
+
215
+ | Hook | Input | Returns | Called |
216
+ |---|---|---|---|
217
+ | `beforeRegister` | `{email, profile?}` | `{allow: boolean, reason?}` | Before an account is created, on `POST /account/register` and on `POST /account/signup/begin`. |
218
+ | `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. |
219
+ | `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. |
220
+
221
+ ### `@jimhoyd/urlcode-admin`
222
+
223
+ | Hook | Input | Returns | Called |
224
+ |---|---|---|---|
225
+ | `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. |
226
+ | `onRegistrationApproved` | `{requestId, accountId, email, actorId, reason}` | ignored | After a registration request is approved. |
227
+ | `onAccountStatusChanged` | `{accountId, status, actorId, reason}` | ignored | After an account is locked or unlocked. |
228
+
229
+ ### Verdicts and failure
230
+
231
+ - **A veto is explicit.** A pre-action hook allows only by returning
232
+ `allow: true`. `allow: false`, or no verdict at all, rejects the operation
233
+ with `403` and the hook's own `reason`, or a generic message when it gave
234
+ none. Nothing is written. A hook that *throws* has not returned a verdict:
235
+ the operation is still refused, but as a generic `500`, so return a verdict
236
+ rather than throwing when you mean to deny.
237
+ - **Broken hooks fail at activation, not at the first request.** A missing
238
+ module, a source path escaping the project, an export that is not a
239
+ function, or `sandbox: true` all throw while the extension activates, naming
240
+ the hook. The site does not start.
241
+ - **A post-action hook cannot undo anything.** `onSignUp`,
242
+ `onDelete`, `onRegistrationApproved` and `onAccountStatusChanged` run after
243
+ the operation has committed. Throwing from one replaces the success response
244
+ with a `500` while the account, approval or status change stands. There is
245
+ no retry and no rollback. Keep them non-throwing: catch your own errors and
246
+ queue the work instead of failing the request.
247
+ - **A hook's message is not a channel to the browser.** Only a pre-action
248
+ `reason` is shown. An uncaught error surfaces as a generic failure.
249
+ - **An edited hook needs a restart.** Activation re-imports the hook's entry
250
+ module, so a reload picks up an edit to that file — but modules it imports
251
+ stay on Node's module cache, exactly as for trusted route functions.
252
+
253
+ ## TypeScript: implementing a new extension
254
+
255
+ Only write an extension when a capability is genuinely absent — not to
256
+ customize one of the three above. An extension is an operator-installed
257
+ package whose host object core activates; it is named in `host.mjs`, never
258
+ in YAML. The contract, the activation inputs, `ExtensionActivation.root`,
259
+ credential headers and the `projectSha256` pin are in
260
+ [EXTENSIONS.md](EXTENSIONS.md) and [TYPESCRIPT.md](TYPESCRIPT.md); the
261
+ `scaffold` export that makes a package work with `init --with` is in
262
+ [EXTENSIONS.md](EXTENSIONS.md#scaffolding-with-init---with).
263
+
264
+ If its screens should be themeable the same way auth's and admin's are, it
265
+ also exports a template namespace (and, if it ships English wording, a
266
+ catalogue) for a host to pass to `createUiExtension`. That is what makes
267
+ `ui/templates/<yourname>/<screen>.html` work in a consumer project without a
268
+ fork.
269
+
270
+ ## What this page does not claim
271
+
272
+ The composition, the refusals and the override path are exercised by
273
+ `test/workspace-scaffold.integration.ts`, which runs `init --with` against the
274
+ built packages, drops a template and a copy catalogue into the generated
275
+ `ui/` directory and asserts both reach a rendered auth screen and a rendered
276
+ admin screen. That runs in-process against the generated host: no HTTP
277
+ listener, TLS proxy, browser or deployed site is exercised, and no published
278
+ npm tarball is checked against this checkout.