@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
@@ -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/STARTERS.md CHANGED
@@ -36,18 +36,30 @@ on every push and pull request once the project is on GitHub.
36
36
  See [organization](ORGANIZATION.md) for choosing your own layout. Defaults allow
37
37
  GET/HEAD and use redirect 302; add configuration only when changing behavior.
38
38
 
39
+ `urlcode init` writes no `package.json`: the route project is route-only, and its
40
+ runtime may be installed globally, in a parent workspace or in a container. Add
41
+ `--manifest` to also write one pinning the runtime at exactly the version that
42
+ generated the project, then run `npm install` in it yourself to install that
43
+ version and produce a lockfile. The CLI never runs a package manager, and no
44
+ upgrade command exists — a pinned version changes when you edit the manifest.
45
+
39
46
  Initialization refuses an existing destination. Own the app in your own repository,
40
47
  keep secrets out of Git, and upgrade the runtime separately without regenerating
41
48
  application files. Add pages, downloads, more functions and business-specific
42
49
  features to this same project. Update tests and the expected route count as it grows.
43
50
  See [readiness](READINESS.md) and [security](FUNCTION-SECURITY.md).
44
51
 
45
- To start an extended site instead, install the extension packages from npm
46
- (`npm install @jimhoyd/urlcode-auth @jimhoyd/urlcode-admin`, published as
47
- `0.1.0-alpha.x` prereleases) in the directory you run from and pass their names: `urlcode init ../my-site --with
48
- auth,admin` writes the same starter under `my-site/app/`, merges each package's
52
+ To start an extended site instead, install the compatible core, UI, auth and
53
+ admin set from npm in the directory you run from; the
54
+ [framework guide](FRAMEWORK.md#the-composition-contract) provides the exact
55
+ `0.4.1` command to use after publication. Pass UI first so its kit activates
56
+ before auth: `urlcode init ../my-site --with ui,auth,admin` writes the same starter under `my-site/app/`, merges each package's
49
57
  routes and declarations into it, and generates one `host.mjs` and README beside
50
- it. The contract each package fulfils is in [extensions](EXTENSIONS.md#scaffolding-with-init---with).
58
+ it, plus a `package.json` pinning the runtime, those packages and their declared
59
+ peers at the versions it just resolved, validated together against every declared
60
+ peer range. Installing them is your explicit `npm install` in that directory.
61
+ The contract each package fulfils is in [extensions](EXTENSIONS.md#scaffolding-with-init---with),
62
+ with `--no-manifest` and `--pin` in [recorded versions](EXTENSIONS.md#recorded-versions).
51
63
 
52
64
  Both paths carry an `AGENTS.md` for repository-aware assistants. `urlcode init`
53
65
  generates it from the installed runtime's capability catalog (the same source as
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
@@ -81,10 +81,12 @@ network. Keys always appear in this order:
81
81
  `--host-file`, `host` counts the operator module's extensions and plugins
82
82
  without activating them.
83
83
  - `routes`: path, methods and handler per route, sorted by path.
84
- - `constraints`: a fixed list that holds for every project (no guest network,
85
- no Node APIs, no regex routes, one handler per route, exact or `{param}`
86
- path segments, subtree mounts only for static and extension routes, no YAML
87
- interpolation, secrets by operator grant only), each with a value and a note.
84
+ - `constraints`: a fixed list that holds for every project (network and Node
85
+ built-ins available to trusted code and withdrawn by `sandbox: true`, no
86
+ regex routes, one handler per route, exact or `{param}` path segments,
87
+ subtree mounts only for static and extension routes, no YAML interpolation,
88
+ injected `env`/`secrets` by operator grant only), each with a value and a
89
+ note spelling out how it differs between the two trust modes.
88
90
  - `targets`: for each capability target (or the one `--target`), which of this
89
91
  project's used features are supported, conditional, refused or unknown.
90
92
  - `commands`: the exact `validate`, `test`, `audit --expect-routes N` (N is
package/docs/VERCEL.md CHANGED
@@ -7,7 +7,8 @@ that is the point of the project format.
7
7
  **This adapter serves native handlers only:** redirects, validated responses,
8
8
  pages, static assets and downloads. `function` and `middleware` routes are
9
9
  refused at activation, trusted or sandboxed alike, not per request, so a deployment cannot
10
- half-work. See [what is not supported](#what-this-adapter-does-not-do).
10
+ half-work, and that is a settled position rather than a pending limitation. See
11
+ [what is not supported](#what-this-adapter-does-not-do).
11
12
 
12
13
  A working project is in [`examples/vercel/`](../examples/vercel/).
13
14
 
@@ -71,13 +72,20 @@ and want it in generated URLs.
71
72
 
72
73
  | Not supported | Why |
73
74
  |---|---|
74
- | `function` routes | They need the self-hosted Node lifecycle; a `sandbox: true` route would additionally spawn worker threads and load the WASM engine on every cold start. Correctness is not the issue; predictable latency is, and it is unmeasured. |
75
+ | `function` routes | They need the self-hosted Node lifecycle; a `sandbox: true` route would additionally spawn worker threads and load the WASM engine on every cold start. Correctness is not the issue; the execution model is — per-route compilation was considered and declined. |
75
76
  | Middleware | Runs in the same execution mode as the route's function, and is refused with it. |
76
77
  | `urlcode serve` operational endpoints | `/_urlcode/health` and `/_urlcode/ready` describe a long-lived process. Use Vercel's own observability. |
77
78
 
78
79
  Each refusal happens at activation with a message naming the route, so you find
79
80
  out on deploy rather than on a request.
80
81
 
82
+ `function` and `middleware` are not coming to this adapter. The supported answer
83
+ is to deploy the project as one trusted Node process — a container or a VM
84
+ running the project as it runs locally — which supports every route type today,
85
+ on any host you like including AWS (ECS, EC2, App Runner). See
86
+ [the decision](OPEN-DECISIONS.md#accepted-one-node-deployment-per-project) and
87
+ [the analysis behind it](archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md).
88
+
81
89
  ## Operating it
82
90
 
83
91
  Every instance activates the project independently: parsing YAML, snapshotting
@@ -26,18 +26,60 @@ 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
+
37
+ The `0.4.1` release is an explicit stable release decision for core, UI, auth
38
+ and admin. Publication moves each package's npm `latest` channel to `0.4.1`, in
39
+ core → UI → auth → admin order, after its release checks pass. A prepared
40
+ manifest or merged release PR does not prove registry publication: use
41
+ `npm run release:status` to inspect the live result before installing the set.
42
+ This alignment does not permanently couple package versions; subsequent
43
+ releases can still select only the packages that changed.
44
+
29
45
  Alpha releases publish under `alpha`; they never automatically move npm
30
- `latest`. Core's historical `latest` remains the stable 0.3.0 baseline until an
31
- explicit stable release decision. Extensions have historical alpha versions on
32
- `latest`; subsequent alpha publication does not keep that channel in lockstep.
33
- Different channel values alone are not drift. Test the install combination you
34
- recommend against peer ranges; a bare install may select an older channel.
35
- `release:status` reports each declared peer floor and whether its current
36
- `latest` and `alpha` satisfy the range.
46
+ `latest`. Stable publication does not move `alpha`, so the two channels can
47
+ legitimately show different versions. Test the install combination you recommend
48
+ against peer ranges. `release:status` reports each declared peer floor and
49
+ whether its current `latest` and `alpha` satisfy the range.
50
+
51
+ After all four `0.4.1` versions are published, install the aligned set with:
52
+
53
+ ```sh
54
+ 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
55
+ ```
56
+
57
+ Bare package names resolve npm's current `latest`; exact application pins and a
58
+ committed lockfile keep an existing application from changing on a new release.
59
+
60
+ ## Generated applications
61
+
62
+ A generated application records its own versions. `urlcode init --with` writes a
63
+ `package.json` pinning the running runtime, the named extensions and their
64
+ declared peers at the exact versions resolved at generation time, after checking
65
+ that set against every declared peer range; `urlcode init --manifest` does the
66
+ same for a route-only project with the runtime alone; `urlcode-auth init` pins
67
+ this package and its peers. Plain `urlcode init` stays route-only and writes no
68
+ manifest, for projects whose runtime is managed elsewhere. Generation never runs
69
+ a package manager: `package-lock.json` exists only after the operator runs
70
+ `npm install` in the generated directory, and a pin taken from a local path or
71
+ tarball reproduces only where that path exists.
72
+
73
+ No upgrade command exists. A generated project moves to new versions by an
74
+ operator editing its manifest and re-installing. The issue that asked for this
75
+ (#212) describes a future command that would choose a tested compatible set,
76
+ show the changes and require explicit alpha selection; nothing here implements
77
+ that, and the pins above are only the groundwork it would need.
37
78
 
38
79
  The standalone `urlcode-template` is an external exact-version consumer: after a
39
80
  runtime release, update its dependency and starter through its own reviewed PR.
40
- It is not automatically released by the monorepo coordinator. The retired
81
+ The coordinator prepares and checks that PR after registry installation succeeds;
82
+ the template is a consumer update, not a fifth npm package. The retired
41
83
  `urlcode-docs`, `urlcode-middleware`, `urlcode-dynamic-link` and `urlcode-short`
42
84
  repositories are not release targets.
43
85
 
@@ -6,6 +6,7 @@ current instructions. See the [current roadmap](../../../ROADMAP.md),
6
6
  Remaining acceptance work is not declared complete by archiving this record.
7
7
 
8
8
  <!-- trust-model-prose: historical-file -->
9
+ <!-- local-links: historical-file -->
9
10
  <!-- guidance-claims: ignore-file -->
10
11
 
11
12
  # Public roadmap
@@ -6,6 +6,7 @@ current instructions. See the [current roadmap](../../../ROADMAP.md),
6
6
  Remaining acceptance work is not declared complete by archiving this record.
7
7
 
8
8
  <!-- trust-model-prose: historical-file -->
9
+ <!-- local-links: historical-file -->
9
10
  <!-- guidance-claims: ignore-file -->
10
11
 
11
12
  # Review: the extension model, its precedents and its alignment
@@ -1,19 +1,39 @@
1
+ # Historical record
2
+
3
+ Archived 2026-09-19, when the proposal was **declined**. This records an earlier
4
+ proposal, not current instructions. The decision it lost to is
5
+ [one Node deployment per project](../../OPEN-DECISIONS.md#accepted-one-node-deployment-per-project);
6
+ see also the [current roadmap](../../../ROADMAP.md) and
7
+ [current contract](../../SPECIFICATION.md). Remaining acceptance work is not
8
+ declared complete by archiving this record.
9
+
10
+ <!-- trust-model-prose: historical-file -->
11
+ <!-- guidance-claims: ignore-file -->
12
+
1
13
  # Spike: compiling `function` routes into their own Lambdas
2
14
 
3
15
  > Review update, 2026-09-19: Current baseline: trusted Node execution is the default, so fresh invocation
4
16
  > state is a guarantee of `sandbox: true` only. AWS/Vercel still reject functions
5
17
  > and middleware in `src/capabilities.ts`; changing the default did not implement
6
- > a provider adapter for them. Re-evaluate the proposed per-route lowering against
7
- > a single trusted Node deployment per project before choosing either. Removed
8
- > link APIs in the analysis below are historical.
18
+ > a provider adapter for them. The per-route lowering was weighed against a
19
+ > single trusted Node deployment per project, and **the Node deployment was
20
+ > chosen** (2026-09-19). Removed link APIs in the analysis below are historical.
9
21
 
10
22
 
11
- Status: proposal, nothing implemented. No code in this repository does any of
12
- this, and nothing here is committed scope.
23
+ Status: **declined 2026-09-19 — kept as the analysis behind that decision, not
24
+ as a plan.** Nothing here is implemented and nothing here is committed scope.
25
+ The maintainer chose the alternative this document weighs itself against: a
26
+ project that uses `function` or `middleware` deploys as **one trusted Node
27
+ process**, and AWS/Vercel keep refusing those routes at activation as a
28
+ deliberate position rather than a gap awaiting an adapter. See
29
+ [open decisions](../../OPEN-DECISIONS.md#accepted-one-node-deployment-per-project).
30
+
31
+ Read on for why, and for what a first attempt would look like if real demand for
32
+ `function` routes on AWS serverless ever appears and reopens the question.
13
33
 
14
34
  AWS already deploys today. `createLambdaHandler` (`src/aws.ts`) runs a project
15
35
  as **one** Lambda behind a Function URL or an API Gateway HTTP API, reading the
16
- same `urlcode.yaml` that runs locally — see [AWS](AWS.md). What it cannot serve
36
+ same `urlcode.yaml` that runs locally — see [AWS](../../AWS.md). What it cannot serve
17
37
  is `function`, `middleware` and `link`, which
18
38
  `activateNativeOnly` (`src/adapters.ts`) refuses for the whole deployment at
19
39
  activation rather than letting individual routes fail per request.
@@ -22,6 +42,120 @@ This spike asks one question: **is the refusal of `function` a fact about
22
42
  Lambda, or a fact about the adapter?** It argues the second, sketches the
23
43
  lowering that follows, and is deliberate about what that lowering costs.
24
44
 
45
+ Section 0 says all of that again without the jargon; sections 1 onward are the
46
+ argument in full.
47
+
48
+ ## 0. In plain terms
49
+
50
+ This section is the whole spike in ordinary language. Everything after it is the
51
+ same argument stated precisely; if the two ever disagree, the precise version
52
+ wins.
53
+
54
+ ### What a `function` route is
55
+
56
+ Most of a URLCode project is description, not code: "this URL redirects there",
57
+ "this URL returns that page", "this URL serves that file". You write those in
58
+ YAML and never write a program. A **`function` route** is the escape hatch for
59
+ the cases YAML cannot describe — you write a small piece of JavaScript, and the
60
+ runtime calls it when a request arrives. `middleware` is the same idea, except
61
+ it runs on the way *to* other routes rather than answering by itself.
62
+
63
+ ### Where that code runs today
64
+
65
+ Two ways, and the project picks per route:
66
+
67
+ - **Trusted (the default).** The code runs directly in the same Node process as
68
+ everything else, at full speed, with the same access the process has. This is
69
+ the default because the person writing the route is usually the same person
70
+ deploying it. See [the decision](../../SPIKE-DEFAULT-TRUST-MODEL.md).
71
+ - **Sandboxed (`sandbox: true`, opt in per route).** The code runs inside a
72
+ small, separate JavaScript engine (QuickJS, compiled to WebAssembly) on a
73
+ worker thread. Inside there it has no filesystem, no network, no environment
74
+ variables and a completely fresh memory space on every single call. It can
75
+ only reach the outside world through *bindings* an operator granted by name.
76
+ See [function security](../../FUNCTION-SECURITY.md).
77
+
78
+ Think of the sandbox as a sealed room inside your own house. Nothing gets in or
79
+ out except through a hatch you deliberately opened, and the room is wiped clean
80
+ between visitors.
81
+
82
+ ### What happens on AWS today
83
+
84
+ Nothing, for these routes. If a project contains any `function` or `middleware`
85
+ route, the AWS adapter refuses **the entire deployment at startup** and names
86
+ what it cannot serve. Everything else — redirects, pages, static files,
87
+ downloads — deploys fine as a single Lambda.
88
+
89
+ That refusal is not a bug or an oversight. It is the runtime declining to
90
+ pretend. The alternative would be accepting the deployment and then failing
91
+ individual requests in production, which is worse.
92
+
93
+ ### Why it refuses
94
+
95
+ The sealed room is expensive to build. Every time AWS starts a fresh copy of
96
+ your Lambda (a "cold start"), that copy would have to spin up worker threads and
97
+ boot a WebAssembly engine *before* it could answer the first request. One
98
+ process trying to serve every route in a project, rebuilding all that machinery
99
+ on every cold start, is a bad trade.
100
+
101
+ The key observation of this spike: that reasoning is about **one process serving
102
+ every route**. It is not actually about AWS. Change the unit of deployment and
103
+ the objection evaporates.
104
+
105
+ ### The idea
106
+
107
+ Stop shipping one Lambda for the whole project. Instead, at build time, emit
108
+ **one Lambda per `function` route** — plus the existing single Lambda for all
109
+ the ordinary declarative routes.
110
+
111
+ Then you do not need the sealed room at all, because each piece of code already
112
+ has an entire AWS process to itself. The process *is* the wall. This is the same
113
+ move URLCode already makes for Cloudflare: when a platform will not run what the
114
+ adapter needs, compile the project ahead of time instead of adapting at runtime.
115
+
116
+ ### The catch — three of them
117
+
118
+ 1. **It is a different kind of safety, not more of the same safety.** A Lambda
119
+ is a genuinely separate box, which is good. But out of the box that box has
120
+ working network access, a writable temp disk, ambient environment variables,
121
+ and an AWS identity (an "IAM role") that can reach real infrastructure. The
122
+ sealed room had none of that. So swapping one for the other is a **trade**,
123
+ not an upgrade — and the documentation would have to say exactly which
124
+ guarantee changed rather than announcing "functions work on AWS now". One
125
+ guarantee is simply lost: the sandbox promises fresh memory on every call,
126
+ and AWS reuses warm containers.
127
+ 2. **URLCode would start generating security-critical infrastructure.** To keep
128
+ each route's permissions tight, the build would have to write out an IAM role
129
+ per route. A generated permissions file is not an implementation detail you
130
+ can wave away with "don't rely on this" — if it is too generous, it is a
131
+ security hole the project authored.
132
+ 3. **It would be an unverified claim wearing a capability's clothes.** Right now
133
+ the project's position on AWS is honest: it reports `deployment: 'unverified'`
134
+ and tells you to deploy the example yourself. Shipping a compiler that emits
135
+ cloud infrastructure nobody on the project has ever actually deployed would
136
+ trade an honest refusal for a bigger unproven promise.
137
+
138
+ ### The alternative already sitting on the table — and now chosen
139
+
140
+ Run the project as **one ordinary Node deployment** — a container or a VM. That
141
+ supports `function` and `middleware` today, with no compiler, no generated
142
+ infrastructure and no new security surface. It costs you the serverless
143
+ operating model: something is always running, and you scale it yourself.
144
+
145
+ So the real decision is not "can this be built" — it probably can. It is:
146
+
147
+ > Is there enough demand for running URLCode `function` routes specifically on
148
+ > AWS serverless to justify this project owning an infrastructure compiler and a
149
+ > second, weaker isolation story?
150
+
151
+ **That question was answered on 2026-09-19: no.** Nobody has produced the demand
152
+ evidence, and the plain Node deployment does the job today, so the Node
153
+ deployment is the supported model and the compiler is not being built. The
154
+ refusal on AWS and Vercel stays, and is now a position rather than a gap. See
155
+ [open decisions](../../OPEN-DECISIONS.md#accepted-one-node-deployment-per-project).
156
+ If that demand ever shows up, §6 below already scopes what a first attempt
157
+ would be.
158
+
25
159
  ## 1. Where the refusal actually comes from
26
160
 
27
161
  `src/capabilities.ts` gives the reason:
@@ -32,7 +166,7 @@ capability === 'function' ? 'isolated functions need worker threads and the WASM
32
166
 
33
167
  That is true of the runtime's *own* mechanism. Isolation for guest code is
34
168
  QuickJS inside WebAssembly, driven from worker threads, with the boundaries
35
- [function security](FUNCTION-SECURITY.md) lists: no `process`, no filesystem,
169
+ [function security](../../FUNCTION-SECURITY.md) lists: no `process`, no filesystem,
36
170
  no sockets, no `fetch`, a fresh guest heap per invocation, and bindings denied
37
171
  unless an operator granted them by exact name.
38
172
 
@@ -49,7 +183,7 @@ Cloudflare already establishes the pattern: where a platform forbids what the
49
183
  adapter needs, URLCode **compiles ahead of time** instead of adapting at
50
184
  runtime. `urlcode build --target cloudflare` (`src/build-cloudflare.ts`) emits
51
185
  an artifact the Worker reads, and refuses at build time anything it cannot
52
- serve, with the route named — see [Cloudflare](CLOUDFLARE.md).
186
+ serve, with the route named — see [Cloudflare](../../CLOUDFLARE.md).
53
187
 
54
188
  The same move for AWS: a build step emits **one Lambda per `function` route**,
55
189
  plus the existing native-handler Lambda for everything else.
@@ -75,6 +209,9 @@ This is not a smaller change than it looks. Three things follow from it.
75
209
 
76
210
  This is the claim most likely to be made too early, so it goes first.
77
211
 
212
+ > **Plainly:** both are real walls, but they are different walls, and neither
213
+ > one is strictly stronger. Read the table as a trade, not a scorecard.
214
+
78
215
  QuickJS-WASM and a Lambda are both real isolation. They are **not the same
79
216
  isolation**, and neither strictly contains the other:
80
217
 
@@ -101,6 +238,12 @@ this project should not make.
101
238
 
102
239
  ### 3.2 `middleware` is the hard part, not `function`
103
240
 
241
+ > **Plainly:** a `function` is a dead end — it answers and the request stops
242
+ > there, so giving it its own Lambda is easy. Middleware is a queue of steps
243
+ > every request walks through on its way somewhere else, and a queue split
244
+ > across separate Lambdas has to be either copied into each one or wired up as
245
+ > a chain of calls. Both hurt, so this spike does not attempt middleware.
246
+
104
247
  `function` lowers cleanly because it is a leaf. `middleware` is a per-request
105
248
  chain, and there are only two ways to lower it, both with a real cost:
106
249
 
@@ -124,6 +267,11 @@ section describes was later removed from core; see
124
267
 
125
268
  ## 4. Emitting infrastructure is a new kind of output
126
269
 
270
+ > **Plainly:** up to now URLCode has only ever produced things that run *inside*
271
+ > a server it was given. Emitting a CloudFormation stack means it starts
272
+ > producing the cloud account's own configuration — including permissions — and
273
+ > becomes responsible for that being correct as AWS changes underneath it.
274
+
127
275
  `examples/aws/template.yaml` is hand-written today. Generating a stack means
128
276
  this project starts owning a surface it has never owned:
129
277
 
@@ -139,7 +287,7 @@ this project starts owning a surface it has never owned:
139
287
 
140
288
  That last point is the one worth holding onto. The project's current position on
141
289
  AWS is **honest**: the capability catalog reports `deployment: 'unverified'` for
142
- every target that is not self-hosted, and [AWS](AWS.md) tells a reader to treat
290
+ every target that is not self-hosted, and [AWS](../../AWS.md) tells a reader to treat
143
291
  the limits as unverified until they deploy the example themselves. A compiler
144
292
  that emits infrastructure nobody has deployed would be a larger unverified claim
145
293
  wearing the clothes of a capability.
@@ -161,7 +309,9 @@ lowering without anything else in the model changing, and `urlcode capabilities
161
309
 
162
310
  ## 6. Proposed scope for a first spike
163
311
 
164
- Narrow, so that the isolation story stays clean and the win is real:
312
+ Narrow, so that the isolation story stays clean and the win is real.
313
+ **Plainly: build the smallest version that proves the idea, and resist every
314
+ tempting extra.**
165
315
 
166
316
  **In:** `function` routes, Function URL only, one Lambda per function route,
167
317
  a generated role per route carrying exactly that route's granted bindings,
@@ -190,13 +340,19 @@ having run it on AWS, and the spike should say so rather than imply otherwise.
190
340
 
191
341
  ## 8. Open questions
192
342
 
343
+ These are the things a reviewer should press on. In plain terms: *is losing the
344
+ fresh-state guarantee acceptable at all; should URLCode write the permissions or
345
+ merely describe them; does splitting into many Lambdas quietly weaken the
346
+ policies a project already declares; and is one Lambda per route even the right
347
+ size of piece?*
348
+
193
349
  - Is warm-container reuse acceptable at all, given the runtime currently
194
350
  *guarantees* fresh per-invocation state? If not, this lowering is wrong for
195
351
  any route that relies on that guarantee, and there is no build-time way to
196
352
  tell which ones do.
197
353
  - Does the generated IAM role belong in URLCode's output, or should the build
198
354
  emit a *description* of the permissions each route needs and leave the role to
199
- the operator — closer to how [function security](FUNCTION-SECURITY.md) already
355
+ the operator — closer to how [function security](../../FUNCTION-SECURITY.md) already
200
356
  keeps grants operator-controlled and outside the checkout?
201
357
  - Does a per-route Lambda change what `policies` can promise? `throttle` on AWS
202
358
  is already `conditional` — "counters are per instance" — and more instances
@@ -204,6 +360,6 @@ having run it on AWS, and the spike should say so rather than imply otherwise.
204
360
  - Is one Lambda per route the right granularity, or one per *project* with a
205
361
  route parameter, which keeps deployment small but reintroduces a shared
206
362
  process?
207
- - What happens to the 6 MB Lambda response limit ([AWS](AWS.md)) for a function
363
+ - What happens to the 6 MB Lambda response limit ([AWS](../../AWS.md)) for a function
208
364
  route that returns a large body — refuse at build time, as the Cloudflare
209
365
  target refuses what it cannot serve?
@@ -1,5 +1,7 @@
1
1
  # Spike: consolidating core, auth, admin and ui into one repo
2
2
 
3
+ <!-- local-links: historical-file -->
4
+
3
5
  > **Closed 2026-09-19: the migration is done, and this document is history.**
4
6
  >
5
7
  > `urlcode-ui`, `urlcode-auth` and `urlcode-admin` are workspace packages under
@@ -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.
@@ -19,6 +19,8 @@ Archived on 2026-09-19 after checking the current code:
19
19
  | [Extension model review](2026-09-19/SPIKE-EXTENSION-MODEL.md) | Historical design rationale | Current extension contract |
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
+ | [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 |
22
24
 
23
25
  Open proposals stay outside the archive with their status stated at the top.
24
26
  The accepted trust-by-default decision remains directly linked from the current
@@ -53,8 +53,16 @@ short form: `middleware: [middleware/headers.mjs]` means
53
53
  `middleware: [{source: middleware/headers.mjs}]`. The path must be
54
54
  project-relative, end in `.mjs` or `.js` and contain no `..` segment.
55
55
  Methods default to GET and HEAD. Function paths resolve from the project root,
56
- not the YAML file's directory. `.js` and `.mjs` ES modules work; TypeScript, Node
57
- APIs, npm imports, network access and filesystem access do not.
56
+ not the YAML file's directory. Modules are ES modules either way: `.mjs` always
57
+ works, while a trusted `.js` module is loaded by Node and therefore needs the
58
+ nearest `package.json` to say `"type": "module"` (a `sandbox: true` route reads
59
+ `.js` as ESM regardless). Serving never transpiles TypeScript; build it first
60
+ with [TypeScript authoring](../TYPESCRIPT-AUTHORING.md). The route above
61
+ declares no `sandbox`, so it runs trusted and in-process: Node APIs, npm
62
+ imports, network and filesystem access are all available to it, exactly as they
63
+ are to any other Node code in the host. Add `sandbox: true` to trade them away
64
+ for isolation -- inside the guest none of them exist. See
65
+ [trust model and sandbox opt-in](../FUNCTION-SECURITY.md).
58
66
 
59
67
  ## 4. Input types and constraints
60
68