@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
@@ -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
@@ -19,6 +19,7 @@ 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 |
22
23
 
23
24
  Open proposals stay outside the archive with their status stated at the top.
24
25
  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
 
@@ -23,7 +23,9 @@ export async function decorate(request, context, next) {
23
23
 
24
24
  Entries run in declared order before the handler and reverse order afterward.
25
25
  Return a Response early to skip downstream code; call `next()` at most once.
26
- Up to 16 middleware entries share one sandbox/deadline and approved route
27
- bindings. Native bodies are opaque; preserving them requires retaining original
28
- status/headers. Return a new Response to replace native content or destination.
26
+ Up to 16 middleware entries share the route's execution mode, one deadline and
27
+ one set of approved route bindings; the mode is the route's `sandbox` field, not
28
+ a per-entry choice. On a `sandbox: true` route native bodies are opaque;
29
+ preserving them requires retaining original status/headers. Return a new
30
+ Response to replace native content or destination.
29
31
  YAML response headers apply last. See [middleware](../MIDDLEWARE.md) for details.
@@ -1,5 +1,7 @@
1
- // Wrap successful JSON function responses in a stable envelope. Native bodies are
2
- // opaque and pass through untouched; only readable JSON is rewritten.
1
+ // Wrap successful JSON function responses in a stable envelope. Only a JSON body
2
+ // this chain can actually read is rewritten; everything else passes through
3
+ // untouched -- on a `sandbox: true` route that includes every native body, which
4
+ // the guest cannot read at all.
3
5
  export default async function envelope(request, context, next) {
4
6
  const response = await next();
5
7
  if (!response.ok || !(response.headers.get('content-type') || '').startsWith('application/json')) return response;