@jimhoyd/urlcode 0.4.1 → 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.
@@ -1,13 +1,14 @@
1
1
  # Release readiness
2
2
 
3
- Status: `0.4.0-alpha.2` (`package.json`) alpha of the extension contract and
4
- agent tooling on top of the `0.3.0` self-hosted release; `0.4.0-alpha.1` is the
5
- most recent alpha actually published; the npm dist-tags for `@jimhoyd/urlcode`
6
- were `latest` = `0.3.0` and `alpha` = `0.4.0-alpha.1` when checked against the
7
- registry on 2026-09-19, so the repository's `0.4.0-alpha.2` is unpublished (see
8
- "Packaging" below). Production approval remains specific to
9
- the workload and deployment environment.
10
- This register describes the current public runtime, not future promises.
3
+ Status: this register records evidence and open gates for the public runtime.
4
+ It deliberately carries no current version table: the manifests are the version
5
+ authority, [VERSION-ALIGNMENT.md](VERSION-ALIGNMENT.md) explains channels and
6
+ ownership, and `npm run release:status` reads live registry and tag state. A
7
+ stable release is a packaging fact. It is not production approval and does not
8
+ perform any gate under "Gates before production approval" below; neither does a
9
+ passing CI run. Production approval remains specific to the workload and
10
+ deployment environment. Registry channels and deployments were not re-checked
11
+ when this register was reconciled on 2026-09-20 (#242).
11
12
  Use the contract and docs from the same pinned commit as your installed runtime.
12
13
 
13
14
  ## What is aligned
@@ -41,7 +42,34 @@ deferred to the post-merge run.
41
42
  | Worker replacement | Repeated guest deadlines shed load and the pool returns to service after backoff, rather than latching off for the life of the process | Bounded by the configured worker count; no cross-process load balancing |
42
43
  | Shutdown | New work rejects; repeated close shares completion | Existing deadlines can still fail during shutdown |
43
44
  | Activation/recovery | Invalid reload retains last-good snapshot; corrupt revision metadata rejects activation | No deployment orchestration |
44
- | Packaging | Packed installation and starter examples tested; sensitive files excluded | `0.3.0` and `0.4.0-alpha.1` are published to npm as `@jimhoyd/urlcode` (`latest` and `alpha` dist-tags respectively; dist-tags verified against the npm registry on 2026-09-19, when the repository stood at the unpublished `0.4.0-alpha.2`). Published extension packages on the same date: `@jimhoyd/urlcode-auth@0.1.0-alpha.2`, `@jimhoyd/urlcode-admin@0.1.0-alpha.2`, `@jimhoyd/urlcode-ui@0.1.0-alpha.4`. (`@jimhoyd/urlcode-short@0.1.0-alpha.1`, `@jimhoyd/urlcode-dynamic-link@0.1.0-alpha.1` and `@jimhoyd/urlcode-middleware@0.1.0-alpha.2` were also published on that date, then retired and unpublished later the same day; all three repositories were deleted. Middleware's withdrawal removed no capability — per-route middleware is native to core.) Observed in passing on that date: auth's dist-tags were split — `alpha` at `0.1.0-alpha.2` while `latest` lagged at `0.1.0-alpha.1`, so a plain `npm install @jimhoyd/urlcode-auth` resolved a build below admin's declared floor. That is resolved: auth and admin both read `latest` = `alpha` = `0.1.0-alpha.3`. `@jimhoyd/urlcode-ui` now carries a split of its own — `alpha` = `0.1.0-alpha.6`, `latest` = `0.1.0-alpha.5` — which is deliberate rather than drift, and safe only because admin's ui floor is exactly `>=0.1.0-alpha.5`. See [VERSION-ALIGNMENT.md](VERSION-ALIGNMENT.md). GitHub Releases attach a Homebrew formula (`urlcode.rb`) for manual copy into a tap, not an automated Homebrew Core/tap publish. No provider adapter guarantee. |
45
+ | Packaging | Packed installation and starter examples tested; sensitive files excluded | Published versions and channels: see VERSION-ALIGNMENT.md and `release:status`; this row asserts none. Dated registry observations are kept under "Dated packaging observations" below. GitHub Releases attach a Homebrew formula (`urlcode.rb`) for manual copy into a tap, not an automated Homebrew Core/tap publish. No provider adapter guarantee. |
46
+
47
+ ## Dated packaging observations
48
+
49
+ Kept as observed; each is true only of its date and none is current status.
50
+ Use `npm run release:status` for the present.
51
+
52
+ - **2026-09-19, registry check.** `@jimhoyd/urlcode` had `latest` = `0.3.0` and
53
+ `alpha` = `0.4.0-alpha.1`; the repository then stood at `0.4.0-alpha.2`, which
54
+ was unpublished. Published extension packages that day:
55
+ `@jimhoyd/urlcode-auth@0.1.0-alpha.2`, `@jimhoyd/urlcode-admin@0.1.0-alpha.2`,
56
+ `@jimhoyd/urlcode-ui@0.1.0-alpha.4`.
57
+ - **2026-09-19, retired packages.** `@jimhoyd/urlcode-short@0.1.0-alpha.1`,
58
+ `@jimhoyd/urlcode-dynamic-link@0.1.0-alpha.1` and
59
+ `@jimhoyd/urlcode-middleware@0.1.0-alpha.2` were published that day, then
60
+ retired and unpublished later the same day; all three repositories were
61
+ deleted. Middleware's withdrawal removed no capability: per-route middleware
62
+ is native to core.
63
+ - **2026-09-19, observed in passing.** auth's dist-tags were split (`alpha` at
64
+ `0.1.0-alpha.2`, `latest` at `0.1.0-alpha.1`), so a plain install resolved a
65
+ build below admin's declared floor.
66
+ - **Later 2026-09-19 or after (the source did not date these; treat as
67
+ superseded).** auth and admin both read `latest` = `alpha` =
68
+ `0.1.0-alpha.3`, resolving the split above; `@jimhoyd/urlcode-ui` carried a
69
+ deliberate `alpha` = `0.1.0-alpha.6` / `latest` = `0.1.0-alpha.5` split, safe
70
+ only because admin's ui floor was exactly `>=0.1.0-alpha.5`. The stable
71
+ `0.4.1` alignment described in [VERSION-ALIGNMENT.md](VERSION-ALIGNMENT.md)
72
+ postdates these.
45
73
 
46
74
  `npm run check:downstream-skills` is a manual, advisory report worth running
47
75
  before a release: it diffs core's `.claude/skills/` copies against copies
@@ -80,7 +108,8 @@ benchmark into a universal throughput claim.
80
108
  extension needs its own backup/restore drill; core has no durable store.
81
109
  4. Alerting and ownership for sustained errors, latency, readiness, dropped logs,
82
110
  disk space, restarts and backups. Pick service objectives for the actual app.
83
- 5. Stable-release support commitments before packaging/public reuse claims.
111
+ 5. Stable-release support commitments before packaging/public reuse claims;
112
+ a stable release exists, but that does not by itself record such commitments.
84
113
  Private security reporting and the current support baseline are documented in
85
114
  SECURITY.md.
86
115
 
@@ -88,7 +117,7 @@ License selection is resolved: URLCode is released under the Apache License 2.0,
88
117
  `package.json` declares it, and the repository carries the full license text.
89
118
  The remaining gates above are engineering and operational, not legal.
90
119
 
91
- The unreleased next-phase source now includes strict bulk/provider interchange,
120
+ The next-phase source (shipped in the alpha releases; see the archived plan) includes strict bulk/provider interchange,
92
121
  local recipes, TypeScript authoring, bounded self-hosted proxy/signals and read-only
93
122
  MCP. Local AWS/Vercel/Cloudflare adapter tests and deployment probe tooling exist,
94
123
  but actual provider deployments remain unverified. Node process/container hosting
@@ -78,8 +78,12 @@ any version tag, then creates one at a time and waits for successful publication
78
78
  and consumer-facing registry installability before releasing dependents. Shared publication concurrency avoids cross-version races.
79
79
  The active immutable-tag rule blocks release tag updates/deletions with no
80
80
  bypass actors; its configuration is in `.github/rulesets/release-tags.json`. No automation needs permission to bypass main checks or approve
81
- its own PR. A repository-scoped GitHub App is the preferred eventual automation
82
- identity; a narrowly scoped fine-grained PAT can support a maintainer script.
81
+ its own PR. Manual package/all release workflows share the coordinator and need
82
+ `RELEASE_AUTOMATION_TOKEN` because events created by the ordinary `GITHUB_TOKEN`
83
+ do not trigger tag publishers. A repository-scoped GitHub App is preferred. A
84
+ fine-grained PAT may select only `urlcode` and `urlcode-template`, with Contents,
85
+ Pull requests and Actions read/write plus Checks read. Neither identity needs
86
+ ruleset bypass, administration, npm secrets or review approval.
83
87
 
84
88
  ## Remaining validation
85
89
 
@@ -174,19 +174,15 @@ with `add_repo` before treating it as settled.
174
174
  settles this explicitly: a hook a project names in an extension's own YAML
175
175
  config (`urlcode-auth`'s `onSignUp`/`beforeRegister`/`onDelete` and the
176
176
  like) is first-party project code in the same category as any
177
- `function`/`middleware` route, trusted and in-process by default, with the
178
- same per-hook `sandbox: true` opt-in. No hardwired always-sandboxed case
179
- for lifecycle hooks specifically. Trusted execution of such a hook needs no
180
- core primitive an extension's own `activate()` already has
181
- `ExtensionActivation.root` and can `import()` the project's module
182
- directly. The isolated half of that opt-in previously had no equivalent:
183
- core's trusted/sandboxed dispatch was wired to route dispatch only, not
184
- exposed to extensions. `@jimhoyd/urlcode/sandbox`'s `SandboxPool` (see
185
- [FUNCTION-SECURITY.md](FUNCTION-SECURITY.md), [TYPESCRIPT.md](TYPESCRIPT.md))
186
- closes that: the same worker/QuickJS engine `FunctionPool` already used for
187
- route dispatch, generalized to explicit `{source, export}` entries/targets
188
- instead of `FunctionRoute`, with no second engine and no "trusted" mode
189
- exported alongside it.
177
+ `function`/`middleware` route and runs trusted and in-process. Core now owns
178
+ the shared extension-hook primitive: it validates hook references, imports
179
+ them during activation, enforces the extension-published input/output JSON
180
+ Schemas, reports those contracts to authoring tools, and includes hook entry
181
+ files in the project revision. Hook sandboxing is not part of this first
182
+ contract; `sandbox: true` is rejected during activation instead of silently
183
+ running trusted. A later sandbox contract must define its serialization and
184
+ capability boundary before it can reuse the worker/QuickJS path described in
185
+ [FUNCTION-SECURITY.md](FUNCTION-SECURITY.md).
190
186
 
191
187
  ## Recommended sequencing
192
188
 
package/docs/TOOLING.md CHANGED
@@ -52,7 +52,7 @@ The tooling API consolidates authoring operations without starting a runtime:
52
52
  - `listRecipes()` and `showRecipe(name)` expose the fixed bundled recipe catalog.
53
53
  - `inspectExtensions({project, hostFile?})` reports each operator-registered
54
54
  extension's name, contract version, targets, credential headers, configuration
55
- and policy JSON Schemas, whether the project declares it, whether its revision
55
+ and policy JSON Schemas, machine-readable project hook contracts, whether the project declares it, whether its revision
56
56
  pin matches and where routes mount or require it, plus the project's declared
57
57
  names. With `hostFile` it executes that trusted operator module under the
58
58
  `--host-file` rules (absolute path, outside the project) and releases it
@@ -26,6 +26,14 @@ must match its manifest. `npm run release:check` rejects stale lockfile versions
26
26
  Unreleased source changes do not require moving a published tag or pretending a
27
27
  new package has already shipped.
28
28
 
29
+ Manual GitHub Actions releases can select `core`, `ui`, `auth`, `admin`, or
30
+ `all`. A single-package release updates only that package's manifest, lock entry,
31
+ changelog and relevant Changesets; core also owns its duplicated CLI/MCP/plugin
32
+ version metadata and downstream starter update. The all-packages action aligns
33
+ every manifest and advances internal peer floors together. Changesets that name
34
+ packages across the selected boundary must be released together rather than
35
+ partially consumed.
36
+
29
37
  The `0.4.1` release is an explicit stable release decision for core, UI, auth
30
38
  and admin. Publication moves each package's npm `latest` channel to `0.4.1`, in
31
39
  core → UI → auth → admin order, after its release checks pass. A prepared
@@ -0,0 +1,116 @@
1
+ # Historical record
2
+
3
+ Archived 2026-09-20 from [open decisions](../../OPEN-DECISIONS.md), which was
4
+ reconciled against its issues that day (#242). These sections record completed
5
+ work and dated observations exactly as they stood on 2026-09-19 (core `db375bf`
6
+ and the package sources of that date); they are not current instructions and
7
+ their version numbers, dist-tags, commits and pins are not current. Versions
8
+ and channels: [version alignment](../../VERSION-ALIGNMENT.md) and
9
+ `npm run release:status`. Archiving does not declare any security, deployment,
10
+ recovery or provider check performed. The live middleware decision remains in
11
+ open decisions.
12
+
13
+ <!-- trust-model-prose: historical-file -->
14
+ <!-- guidance-claims: ignore-file -->
15
+ <!-- local-links: historical-file -->
16
+
17
+ ## Closed questions removed from the active list
18
+
19
+ - Publishing convention is recorded in [version alignment](../../VERSION-ALIGNMENT.md):
20
+ publishable manifests on main, releases through reviewed tags/workflows.
21
+ - Core `0.4.0-alpha.2` and current extension releases exist; publishing that
22
+ already-shipped version is not a next step.
23
+ - Auth/admin kit adoption and shared form helpers are implemented in their code.
24
+ - The UI primitive fallback is retired, which settles the question this table
25
+ carried. Auth and admin now render every screen through the kit and refuse
26
+ activation without it (`packages/auth/src/auth.ts`: "there is no
27
+ shared-primitive fallback"), so "keep both" no longer describes the code.
28
+ - The template pins `0.4.0-alpha.2`. Its skill differences were read against that
29
+ pin: omitted handlers and advice about the removed management API are stale,
30
+ not intentional older-version behavior.
31
+ - The guidance checks run through `npm run check` inside `verify`; a regex check
32
+ is not a schema validator for every example. Extending its coverage is tracked
33
+ separately, not a reason to weaken review or bypass required checks.
34
+
35
+ ## Done: the monorepo migration is complete
36
+
37
+ **Closed 2026-09-19.** `urlcode-ui`, `urlcode-auth` and `urlcode-admin` are
38
+ workspace packages under `packages/`, and all three have been released from
39
+ this repository — `@jimhoyd/urlcode-ui@0.1.0-alpha.6`,
40
+ `@jimhoyd/urlcode-auth@0.1.0-alpha.6`, `@jimhoyd/urlcode-admin@0.1.0-alpha.4`,
41
+ each on `alpha` with `latest` deliberately held behind. Core's dist-tags are
42
+ unchanged. The three source repositories are gone; their history survives only
43
+ as verified `git bundle`s, because the repository allows squash merges only and
44
+ the imported commits did not survive onto `main`.
45
+
46
+ The operational runbook is [DEVELOPMENT-PIPELINE.md](../../DEVELOPMENT-PIPELINE.md)
47
+ and [RELEASE-SECURITY.md](../../RELEASE-SECURITY.md). The plan itself is archived at
48
+ [archive/2026-09-19/SPIKE-MONOREPO.md](../2026-09-19/SPIKE-MONOREPO.md),
49
+ whose closing note records what the plan got wrong — chiefly that its
50
+ strongest argument, the reach of the enforcing checks, only became true after
51
+ both checkers were changed to discover workspace packages.
52
+
53
+ The section below is kept for the middleware decision it records, which is
54
+ still the reason there is no `packages/middleware`.
55
+
56
+ ## Accepted: monorepo first — middleware withdrawn rather than consolidated
57
+
58
+ The maintainer confirmed that monorepo work is starting now. The earlier
59
+ recommendation to keep repositories separate for now is superseded.
60
+
61
+ **Reversed 2026-09-19: the middleware half of this decision no longer applies.**
62
+ This section used to say "move middleware into the monorepo as its own package
63
+ first, preserving its existing API and behavior," and explicitly: "do not
64
+ unpublish or retire the middleware package as part of the initial move."
65
+ That instruction was overtaken. `@jimhoyd/urlcode-middleware` has been
66
+ **unpublished** from npm at `0.1.0-alpha.2` and `jimhoyd-com/urlcode-middleware`
67
+ **deleted** — the package was withdrawn outright instead of migrated, so there
68
+ is no `packages/middleware` to create and no subsequent fold-into-core step.
69
+ The reversal is recorded here rather than deleted because the instruction it
70
+ replaces was explicit, and a reader who remembers it should be able to see that
71
+ it was changed deliberately and not simply forgotten.
72
+
73
+ Nothing was lost in capability terms: per-route middleware is **native to
74
+ core** via the `middleware:` array ([MIDDLEWARE.md](../../MIDDLEWARE.md)), and the
75
+ deleted package only ever offered the same behavior through the extension
76
+ seam. Trust remains the default and explicit sandbox choices keep their
77
+ meaning. The generic extension wrapping hook (`ExtensionInstance.middleware`,
78
+ `RuntimeExtension.cacheSensitive`) stays in core's contract for other
79
+ extensions — it is no longer exercised by any shipped package, which is worth
80
+ knowing before it is assumed to be covered. Static targets continue rejecting
81
+ request-time middleware because there is no server to run it.
82
+
83
+ Migration scope is therefore **core, auth, admin and UI**. Template and the
84
+ distribution tap stay outside that package move. The observed stale peer pins
85
+ and checkout-limited guidance checks strengthen the case for shared
86
+ verification: carry those checks across the new package paths, since merely
87
+ moving files does not prove every generated skill is covered. The cleanup PRs
88
+ that superseded the earlier zero-open-PR survey have since merged, and a fresh
89
+ survey again reports zero open pull requests across all four in-scope
90
+ repositories — re-run it per repository immediately before that repository
91
+ moves rather than trusting this line.
92
+
93
+ The [archived monorepo plan](../2026-09-19/SPIKE-MONOREPO.md) records
94
+ migration context.
95
+ [Issue 172](https://github.com/jimhoyd-com/urlcode/issues/172), which tracked
96
+ "consolidate middleware into core after moving it into the monorepo," was
97
+ **closed on 2026-09-19** as moot — there was nothing left to consolidate.
98
+ Migration starting is not a claim that it has landed.
99
+
100
+ ## Source review baseline
101
+
102
+ | Repository | Reviewed commit | Code checked |
103
+ |---|---|---|
104
+ | core | `db375bf` | Runtime dispatch, schema normalization, capabilities, static compiler, prerender, MCP and resource generators |
105
+ | auth | `71957dd` | Lifecycle hooks, UI rendering and shared helper imports |
106
+ | admin | `f3b4882` | UI rendering, auth-service integration and shared helper imports |
107
+ | UI | `0e96f7f` | Shared forms, kit/host exports and copied core contract |
108
+ | ~~middleware~~ | `f201f4b` | Extension wrapping, per-entry sandbox dispatch and scaffolding — **repository deleted 2026-09-19; this baseline is unreachable except through the local `urlcode-middleware.bundle`** |
109
+ | template | `4e09e50` | Exact core pin, generated guide and both vendored skills |
110
+
111
+ The Homebrew tap (`73eaaef`) still selects stable core `0.3.0`; its old trust
112
+ behavior belongs to that pin and must not be rewritten as alpha.2 behavior.
113
+ The other organization tap and Scoop bucket contain Gitroll, not URLCode.
114
+ The deleted documentation repository is historical context, not a second source
115
+ of current contracts; its former GitHub links no longer resolve. This review is targeted source inspection, not an audit of
116
+ every execution path or an independent security assessment.
@@ -20,6 +20,7 @@ Archived on 2026-09-19 after checking the current code:
20
20
  | [Usability review](2026-09-19/USABILITY-REVIEW.md) | Predates trusted execution and retired packages | New benchmark observations |
21
21
  | [Management security](2026-09-19/MANAGEMENT-SECURITY.md) | Describes the removed core link-management API | No current core replacement |
22
22
  | [Lambda compile spike](2026-09-19/SPIKE-LAMBDA-COMPILE.md) | Proposal declined 2026-09-19 in favour of one Node deployment per project | Open decisions; AWS and Vercel guides |
23
+ | [Completed decisions (2026-09-20)](2026-09-20/OPEN-DECISIONS-COMPLETED.md) | Closed questions, the completed monorepo migration and the 2026-09-19 source baseline, moved out of open decisions (#242) | Current open decisions and release readiness |
23
24
 
24
25
  Open proposals stay outside the archive with their status stated at the top.
25
26
  The accepted trust-by-default decision remains directly linked from the current
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: 16 documents, about 52,663 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
 
@@ -540,9 +540,10 @@ either `true` or `false`) or `sandbox: true` is enough to silence it.
540
540
 
541
541
  The same judgment call applies to a project-level lifecycle hook an
542
542
  extension invokes (`onSignUp`, `beforeRegister` and the like) — it is
543
- first-party project code with the same trusted-by-default rule and the same
544
- `sandbox: true` opt-in as any `function`/`middleware` route, no special
545
- case. See [EXTENSIONS.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md#project-level-lifecycle-hooks).
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).
546
547
 
547
548
  Guest TypeScript needs `build-typescript --project SOURCE --out NEW_DIRECTORY`
548
549
  before serving. Only the emitted `.js`/`.mjs` executes, in QuickJS for a
@@ -561,7 +562,7 @@ origin grants pinned to the project revision. These are self-hosted features;
561
562
  providers refuse them. Signals are bounded best effort with drops, no retries
562
563
  or persistence. Never turn a user request into an implicit network grant.
563
564
 
564
- Before using a feature, ask `urlcode capabilities <name>` for its constraints, grants and target support and `urlcode schema <path>` for only that YAML fragment (MCP: `get_capability`, `get_schema`), instead of guessing.
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.
565
566
  The [tooling SDK and stdio MCP](https://github.com/jimhoyd-com/urlcode/blob/main/docs/TOOLING.md) inspect, validate, explain and preview
566
567
  without guest execution, environment reads or writes. Run `urlcode explain /route`
567
568
  to check effective methods, policies and cache outcome, and `urlcode manifest`
@@ -2702,29 +2703,18 @@ Node adapter conformance is not a live-provider deployment claim.
2702
2703
 
2703
2704
  ### Project-level lifecycle hooks
2704
2705
 
2705
- `authorize` and `middleware` let core call *into* an extension. They do not
2706
- let a project hand its own code *to* an extension to run at a defined point.
2707
- That gap matters once an extension has meaningful lifecycle events
2708
- registration, deletion, an administrative action, a link resolution, and so
2709
- on. Presentation already has a standard layering mechanism for this: a
2710
- project customizes an extension's *look* through `urlcode-ui`'s
2711
- `copy`/`extra.css`/`templates` without forking it. Behavior needs the same
2712
- standard, or every extension author (and every project depending on one)
2713
- either reinvents it or forks the extension. `urlcode-auth` and
2714
- `urlcode-admin` independently hit this gap (auth/#35, admin/#32); auth's own
2715
- [SPIKE-AUTH.md](https://github.com/jimhoyd-com/urlcode/blob/main/packages/auth/docs/SPIKE-AUTH.md)
2716
- already scoped a shape for `onSignUp`/`beforeRegister`/`onDelete`.
2717
-
2718
- **The pattern.** An extension with lifecycle events an author judges worth
2719
- exposing should let the project name its own function in the extension's own
2720
- `config`, using the same source shape `function`/`middleware` routes already
2721
- use (a string path, or `{source, export, args}` — `schemas/urlcode.schema.json`),
2722
- and add its own `sandbox` boolean next to it (below). The extension's own
2723
- `activate()` reads that config, and its own runtime dispatch — not a new core
2724
- primitive, an ordinary call the extension package makes with the request
2725
- context it already has — invokes the named function at the lifecycle point
2726
- it defines, with a typed input and a typed verdict the extension's own
2727
- schema documents. For example, an auth-style extension might declare:
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:
2728
2718
 
2729
2719
  ```yaml
2730
2720
  extensions:
@@ -2740,76 +2730,51 @@ extensions:
2740
2730
  ```
2741
2731
 
2742
2732
  with `beforeRegister` called before an account is created, given a typed
2743
- `{email, traits}`-shaped input and returning a typed verdict (`{allow: true}`
2733
+ `{email, profile?}` input and returning a typed verdict (`{allow: true}`
2744
2734
  or `{allow: false, reason}`), and `onSignUp` called after, for side effects
2745
- such as provisioning a workspace. The exact hook names, input/verdict shapes
2746
- and invocation points are the extension's own design `hooks` is not a core
2747
- schema key — but the source/export/sandbox shape, and the fact that this is
2748
- project code the extension calls rather than an operator callback in
2749
- `host.mjs`, should be consistent across extensions so an author who has
2750
- learned one has learned the pattern.
2751
-
2752
- **Trust and execution mode.** Project-level lifecycle hooks are first-party
2753
- project code, the same category as any `function`/`middleware` route, and
2754
- follow the same trusted-by-default rule with no special case
2755
- (docs/SPIKE-DEFAULT-TRUST-MODEL.md, [FUNCTION-SECURITY.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/FUNCTION-SECURITY.md)):
2756
- trusted, in-process execution by default; a project sets `sandbox: true` on
2757
- a given hook to opt that hook into isolated QuickJS/WASM execution, exactly
2758
- the mechanism `function`/`middleware` routes already use and for the same
2759
- reason the hook's own code, not the trustworthiness of whatever triggered
2760
- it, is what calls for isolation (docs/AI-AUTHORING.md's "Deciding when a
2761
- route needs `sandbox: true`"). This was raised as an open question — whether
2762
- a lifecycle hook should always run sandboxed because it makes a
2763
- security-relevant decision and settled the other way: uniformity with the
2764
- rest of the trust model was chosen over hardwiring isolation for lifecycle
2765
- hooks specifically, the same "no special case" call already made between
2766
- `function` and `middleware` trust. A `beforeRegister` hook enforcing "only
2767
- `@acme.com` may register" is the project's own governance rule over its own
2768
- signup flow; it is not more dangerous than any other route the project
2769
- wrote, and does not get a different default.
2770
-
2771
- Core's own trusted/sandboxed dispatch (`TrustedFunctions`/`FunctionPool`,
2772
- `src/runtime.ts`) is wired to route dispatch, not exposed to extensions — but
2773
- each half of a hook's `sandbox: true` opt-in has its own answer:
2774
-
2775
- - **Trusted (the default, no `sandbox: true`).** No core primitive is needed
2776
- or provided: this is ordinary first-party project code, and the
2777
- extension's own `activate()` already has `ExtensionActivation.root` to
2778
- resolve the hook's `source` against and can `import()` it directly, the
2779
- same way any trusted `function`/`middleware` route does. Do that import
2780
- with a per-activation cache-busting query, the way core's own trusted
2781
- route activation does (`src/trusted-functions.ts`): Node's ESM loader
2782
- caches a resolved module forever by URL, so a plain `import()` of the
2783
- unchanged file URL makes a second activation in the same process keep
2784
- serving the hook code that was on disk at the first one
2785
- (jimhoyd-com/urlcode#198). Only the hook's **entry** module is refreshed
2786
- this way — modules the hook itself imports stay on Node's module cache,
2787
- the same limitation the trusted route path has, so a change to a hook's
2788
- own dependency still needs a process restart.
2789
- - **Sandboxed (`sandbox: true`).** `@jimhoyd/urlcode/sandbox` exports
2790
- `SandboxPool`, the same QuickJS/worker-thread engine that backs a
2791
- sandboxed `function`/`middleware` route — the identical module-allowlist
2792
- walk, memory/stack limits, two-layer deadline enforcement, `maxBytes` and
2793
- response-shape validation, with no separate or weaker engine for
2794
- extensions. It takes an explicit list of `{source, export}` entries
2795
- (resolve a hook's `source` string with the re-exported `functionFile()`,
2796
- the same resolution/validation a native route's `source` gets) instead of
2797
- anything route/YAML-shaped, and `execute({entry, chain}, request, context)`
2798
- in place of a `FunctionRoute`. There is no "trusted" mode exported
2799
- alongside it — `SandboxPool` is only ever the isolated path; see
2800
- [FUNCTION-SECURITY.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/FUNCTION-SECURITY.md) and
2801
- [TYPESCRIPT.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/TYPESCRIPT.md) for the full contract.
2802
-
2803
- An extension honoring a hook's `sandbox: true` is expected to actually
2804
- isolate that invocation through `SandboxPool` now that the primitive exists
2805
- (or document plainly that it does not yet, rather than accepting the field
2806
- and silently running it trusted) — say which, in the extension's own docs,
2807
- so an author reading them is not misled about what opt-in exists.
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.
2808
2773
 
2809
2774
  ### Discovering schemas
2810
2775
 
2811
2776
  Each registration carries the JSON Schemas that validate its `config` block and
2812
- 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
2813
2778
  the project's own declarations so an author can see what a mount accepts:
2814
2779
 
2815
2780
  ```sh
@@ -2818,6 +2783,7 @@ urlcode extensions --project ./site --host-file /absolute/operator/host.mjs [--j
2818
2783
 
2819
2784
  For every registration in the host file it reports the name, contract version,
2820
2785
  targets, credential headers, configuration schema, policy schema (if any),
2786
+ declared hook names, kinds, descriptions and input/output schemas,
2821
2787
  whether the project declares it, whether its `projectSha256` matches the current
2822
2788
  revision, the routes that mount it and the routes whose policies require it.
2823
2789
  Declared names the host does not register are listed as unregistered. The command
@@ -3162,15 +3128,24 @@ extensions:
3162
3128
  onSignUp: ./hooks/on-signup.mjs
3163
3129
  ```
3164
3130
 
3165
- `hooks` is not a core schema key. It is each package's own config, validated
3166
- by that package's schema before `activate()`, which is why `get_extensions` is
3167
- the place to check what a given version accepts.
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.
3168
3135
 
3169
3136
  Hooks are first-party project code and run **trusted and in-process**, the
3170
3137
  same default `function` and `middleware` routes have
3171
- ([FUNCTION-SECURITY.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/FUNCTION-SECURITY.md)). Neither package implements
3172
- sandboxed hook execution yet; `sandbox: true` on a hook is rejected loudly at
3173
- activation rather than accepted and silently run trusted.
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.
3174
3149
 
3175
3150
  #### `@jimhoyd/urlcode-auth`
3176
3151
 
@@ -3338,14 +3313,11 @@ machinery either way.
3338
3313
 
3339
3314
  This engine — worker spawning, the module-allowlist walk, the two-layer
3340
3315
  deadline, `maxBytes` and response-shape validation — is one implementation
3341
- shared by route dispatch and by `@jimhoyd/urlcode/sandbox`'s `SandboxPool`,
3342
- the public primitive an extension package uses to run a project-supplied hook
3343
- through this exact isolation when the project's own config declares
3344
- `sandbox: true` on it (see [EXTENSIONS.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md#project-level-lifecycle-hooks)
3345
- and [TYPESCRIPT.md](https://github.com/jimhoyd-com/urlcode/blob/main/docs/TYPESCRIPT.md)). Every guarantee above applies identically
3346
- through that entry point; there is no separate, weaker sandbox for extensions
3347
- to reach for, and no "trusted" mode exported there — trusted execution needs
3348
- no primitive at all (docs/SPIKE-DEFAULT-TRUST-MODEL.md).
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).
3349
3321
 
3350
3322
  ### What the trusted default can and can't do
3351
3323
 
package/llms.txt CHANGED
@@ -66,7 +66,7 @@ fields or bypass target limits or operator grants. See [the design principle](do
66
66
  - [Interchange](docs/INTERCHANGE.md), [bulk import](docs/BULK.md), [recipes](docs/RECIPES.md) (`urlcode recipes search`, `examples search`), [TypeScript guests](docs/TYPESCRIPT-AUTHORING.md).
67
67
 
68
68
  ## Extensions (accounts, administration, presentation)
69
- - [Extensions](docs/EXTENSIONS.md): `extensions.<name>` blocks, `extension` mounts, `policies.extensions`, the operator host file, `@jimhoyd/urlcode/extensions`. Project-level lifecycle hooks run trusted via plain `import()`, or sandboxed via `@jimhoyd/urlcode/sandbox`'s `SandboxPool`.
69
+ - [Extensions](docs/EXTENSIONS.md): `extensions.<name>` blocks, mounts, policies, operator registration, the shared trusted hook primitive and its machine-readable hook contracts. UI/auth/admin project hooks run trusted in-process; contract v1 rejects `sandbox: true`.
70
70
  - [Composing a site](docs/COMPOSING-A-SITE.md): what `urlcode init site --with ui,auth,admin` wires, which `--with` combinations are supported, the presentation override paths under `ui/`, and per-package lifecycle hook input, verdict, timing and failure semantics. Separates declarative configuration, project functions and the TypeScript a new extension needs.
71
71
  - [urlcode-auth](packages/auth): npm: @jimhoyd/urlcode-auth; accounts, sessions, MFA, roles, account page; its own llms.txt. Lives in this repository as a workspace package.
72
72
  - [urlcode-admin](packages/admin): npm: @jimhoyd/urlcode-admin; users, sessions, roles, audit, cases; its own llms.txt. Lives in this repository as a workspace package.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jimhoyd/urlcode",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Portable runtime for programmable URL behavior",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "urlcode",
3
3
  "description": "Authoring and operating URLCode projects: the implemented YAML contract, capability limits, deployment and verification commands for the pinned runtime revision.",
4
- "version": "0.4.1",
4
+ "version": "0.4.2",
5
5
  "author": {
6
6
  "name": "jimhoyd-com",
7
7
  "url": "https://github.com/jimhoyd-com"
@@ -30,6 +30,11 @@ Start with `urlcode context --project <dir> --budget 4000`, then retrieve the
30
30
  capability, schema fragment, recipe or example relevant to the change. Use the
31
31
  read-only MCP equivalents when available. `llms.txt` is the index; read the
32
32
  matching task guide from `docs/` when a query needs more explanation.
33
+ When the project has an operator host file, inspect `urlcode extensions
34
+ --project <dir> --host-file <absolute-file> --json` (MCP: `get_extensions`)
35
+ before writing extension configuration or project hooks. The report is the
36
+ machine-readable source for config/policy schemas and hook names, purposes and
37
+ input/output schemas.
33
38
  `docs/SPECIFICATION.md` and `schemas/urlcode.schema.json` resolve contract
34
39
  questions. Archived plans are historical, not valid YAML guidance.
35
40
 
@@ -49,6 +54,11 @@ questions. Archived plans are historical, not valid YAML guidance.
49
54
  - Create every referenced module, page and asset **before** validating. All
50
55
  source paths resolve from the project root. Trusted modules can import Node built-ins and npm packages;
51
56
  only `sandbox: true` modules are restricted to the relative snapshotted graph.
57
+ - Customize installed extensions in this order: declarative configuration;
58
+ `urlcode-ui` copy, theme, template and CSS overrides; a hook declared by the
59
+ extension; a new extension only when the installed contract cannot express
60
+ the behavior. Extension hooks run trusted in-process and reject `sandbox:
61
+ true` in contract v1.
52
62
  - Write exact response fixtures for success and failure, covering every active
53
63
  method, middleware behavior, HEAD, and any range or cache semantics.
54
64
  - Follow `docs/BEST-PRACTICES.md` for layout and readability as the project grows.