@jimhoyd/urlcode 0.4.0-alpha.1 → 0.4.0-alpha.3
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.
- package/.claude/skills/urlcode-authoring/SKILL.md +36 -22
- package/.claude/skills/urlcode-operations/SKILL.md +16 -22
- package/.claude-plugin/marketplace.json +1 -1
- package/CONTRIBUTING.md +20 -16
- package/README.md +83 -80
- package/ROADMAP.md +61 -331
- package/SECURITY.md +23 -8
- package/dist/BUILD-MANIFEST.json +42 -47
- package/dist/agents-guide.js +18 -18
- package/dist/build-static.js +136 -0
- package/dist/capabilities.js +85 -31
- package/dist/capability-query.js +0 -1
- package/dist/cli.js +24 -32
- package/dist/compliance-rules/baseline.js +2 -10
- package/dist/compliance-rules/privacy.js +5 -16
- package/dist/compliance-rules/shared.js +0 -2
- package/dist/compliance.js +6 -8
- package/dist/config.js +15 -8
- package/dist/context.js +9 -10
- package/dist/examples.js +2 -2
- package/dist/explain-cli.js +7 -5
- package/dist/explain.js +10 -5
- package/dist/extensions.js +61 -2
- package/dist/function-sources.js +34 -2
- package/dist/function-worker.js +3 -1
- package/dist/functions.js +84 -13
- package/dist/guest-api.js +29 -3
- package/dist/index.js +3 -6
- package/dist/manifest.js +11 -7
- package/dist/mcp-authoring.js +2 -2
- package/dist/mcp.js +1 -1
- package/dist/observability.js +1 -21
- package/dist/policies/cache.js +8 -3
- package/dist/policies.js +3 -1
- package/dist/policy.js +28 -9
- package/dist/prerender.js +4 -0
- package/dist/project-tests.js +3 -3
- package/dist/readiness.js +35 -9
- package/dist/route-diff.js +12 -5
- package/dist/router.js +5 -7
- package/dist/runtime.js +77 -58
- package/dist/sandbox.js +48 -0
- package/dist/scaffold.js +0 -0
- package/dist/scripts/operational-drills.js +12 -54
- package/dist/server.js +3 -29
- package/dist/tooling.js +1 -1
- package/dist/trusted-functions.js +210 -0
- package/dist/types/build-static.d.ts +43 -0
- package/dist/types/capabilities.d.ts +14 -5
- package/dist/types/compliance-rules/shared.d.ts +0 -2
- package/dist/types/compliance.d.ts +0 -3
- package/dist/types/config.d.ts +2 -1
- package/dist/types/context.d.ts +2 -1
- package/dist/types/examples.d.ts +1 -1
- package/dist/types/explain.d.ts +6 -0
- package/dist/types/extensions.d.ts +55 -0
- package/dist/types/function-sources.d.ts +4 -0
- package/dist/types/functions.d.ts +48 -5
- package/dist/types/guest-api.d.ts +1 -0
- package/dist/types/index.d.ts +3 -6
- package/dist/types/manifest.d.ts +5 -3
- package/dist/types/observability.d.ts +1 -14
- package/dist/types/project-tests.d.ts +1 -2
- package/dist/types/readiness.d.ts +12 -3
- package/dist/types/router.d.ts +2 -1
- package/dist/types/runtime.d.ts +0 -27
- package/dist/types/sandbox.d.ts +12 -0
- package/dist/types/scaffold.d.ts +0 -2
- package/dist/types/server.d.ts +1 -4
- package/dist/types/tooling.d.ts +3 -3
- package/dist/types/trusted-functions.d.ts +29 -0
- package/dist/types/types.d.ts +22 -8
- package/dist/types/verify-deployment.d.ts +2 -2
- package/dist/types.js +25 -5
- package/dist/typescript-authoring.js +67 -18
- package/dist/verify-deployment.js +3 -3
- package/docs/AI-AUTHORING.md +109 -10
- package/docs/ASSETS.md +2 -1
- package/docs/AUTH-BACKUP.md +32 -0
- package/docs/AWS.md +4 -4
- package/docs/BEST-PRACTICES.md +17 -8
- package/docs/CAPABILITIES.md +30 -17
- package/docs/CAPACITY.md +128 -31
- package/docs/CI-FOLLOWUP-2026-09-19.md +97 -0
- package/docs/CI-RELEASE-AUDIT-2026-09-19.md +322 -0
- package/docs/CI.md +8 -3
- package/docs/CLOUDFLARE.md +1 -2
- package/docs/CODEBASE-AUDIT-2026-09-20.md +278 -0
- package/docs/COMPLIANCE.md +6 -9
- package/docs/DEPLOYMENT-CHECKS.md +1 -1
- package/docs/DEVELOPMENT-PIPELINE.md +181 -0
- package/docs/EXTENSIONS.md +193 -11
- package/docs/FRAMEWORK.md +53 -33
- package/docs/FUNCTION-SECURITY.md +173 -32
- package/docs/INSTALL.md +0 -5
- package/docs/LOAD-TESTING.md +4 -4
- package/docs/LOCAL-DEVELOPMENT.md +3 -0
- package/docs/MIDDLEWARE.md +27 -16
- package/docs/MONITORING.md +2 -19
- package/docs/OBSERVABILITY.md +7 -16
- package/docs/OPEN-DECISIONS.md +184 -0
- package/docs/OPERATIONAL-PROOF.md +26 -30
- package/docs/OPERATIONS.md +23 -32
- package/docs/POLICIES.md +27 -8
- package/docs/PRERENDER.md +25 -13
- package/docs/PROJECT-DIRECTION.md +35 -10
- package/docs/READINESS.md +6 -2
- package/docs/README.md +20 -41
- package/docs/RELEASE-0.4.0-alpha.3.md +50 -0
- package/docs/RELEASE-READINESS.md +50 -30
- package/docs/RELEASE-SECURITY.md +72 -86
- package/docs/RESILIENCE.md +16 -15
- package/docs/ROUTING.md +7 -10
- package/docs/SANDBOX-REVIEW.md +19 -6
- package/docs/SCAFFOLDING.md +0 -2
- package/docs/SECURITY-AUDIT.md +42 -2
- package/docs/SPECIFICATION.md +63 -27
- package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +288 -0
- package/docs/SPIKE-BUSINESS-SUITE.md +16 -0
- package/docs/SPIKE-CORE-LAYERING.md +368 -0
- package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +211 -0
- package/docs/SPIKE-LAMBDA-COMPILE.md +12 -2
- package/docs/STARTERS.md +1 -1
- package/docs/STATIC.md +105 -0
- package/docs/TOOLING.md +17 -12
- package/docs/TUNNELS.md +0 -3
- package/docs/TYPESCRIPT-AUTHORING.md +32 -12
- package/docs/TYPESCRIPT.md +25 -4
- package/docs/VERCEL.md +4 -5
- package/docs/VERSION-ALIGNMENT.md +46 -0
- package/docs/YAML-GUIDE.md +0 -3
- package/docs/YAML-REFERENCE.md +16 -6
- package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +68 -0
- package/docs/{MANAGEMENT-SECURITY.md → archive/2026-09-19/MANAGEMENT-SECURITY.md} +20 -0
- package/docs/{NEXT-PHASE-PLAN.md → archive/2026-09-19/NEXT-PHASE-PLAN.md} +26 -8
- package/docs/{NEXT-STEPS.md → archive/2026-09-19/NEXT-STEPS.md} +274 -43
- package/docs/archive/2026-09-19/OPEN-DECISIONS.md +277 -0
- package/docs/archive/2026-09-19/RELEASE-SECURITY.md +186 -0
- package/docs/archive/2026-09-19/ROADMAP.md +386 -0
- package/docs/{SPIKE-EXTENSION-MODEL.md → archive/2026-09-19/SPIKE-EXTENSION-MODEL.md} +12 -2
- package/docs/{SPIKE-EXTENSIONS.md → archive/2026-09-19/SPIKE-EXTENSIONS.md} +28 -12
- package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +776 -0
- package/docs/{USABILITY-REVIEW.md → archive/2026-09-19/USABILITY-REVIEW.md} +18 -2
- package/docs/archive/README.md +27 -0
- package/docs/policies/agents.md +1 -1
- package/docs/policies/cache.md +13 -0
- package/docs/policies/compression.md +3 -2
- package/docs/policies/security.md +3 -2
- package/examples/assets/example.yaml +2 -2
- package/examples/aws/example.yaml +1 -1
- package/examples/cloudflare/example.yaml +1 -1
- package/examples/conditions/example.yaml +1 -1
- package/examples/cookbook/example.yaml +1 -1
- package/examples/cookbook/middleware/bucket.mjs +12 -2
- package/examples/cookbook/middleware/locale.mjs +7 -3
- package/examples/cookbook/route-index.json +1 -1
- package/examples/cookbook/routes/middleware.yaml +1 -1
- package/examples/egress/example.yaml +1 -1
- package/examples/extensions/example.yaml +1 -1
- package/examples/prerender/README.md +14 -6
- package/examples/prerender/example.yaml +1 -1
- package/examples/prerender/functions/page.mjs +4 -2
- package/examples/prerender/middleware/template.mjs +1 -1
- package/examples/prerender/prerender.mjs +1 -1
- package/examples/prerender/urlcode.yaml +8 -4
- package/examples/provider-conformance/example.yaml +1 -1
- package/examples/vercel/example.yaml +1 -1
- package/llms-full.txt +686 -239
- package/llms.txt +27 -15
- package/package.json +32 -5
- package/packaging/claude-plugin/.claude-plugin/plugin.json +2 -2
- package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +36 -22
- package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +16 -22
- package/recipes/authenticated-json-api/README.md +4 -3
- package/recipes/authenticated-json-api/functions/profile.mjs +2 -1
- package/recipes/authenticated-json-api/recipe.yaml +2 -2
- package/recipes/contact-form/functions/contact.mjs +2 -1
- package/recipes/contact-form/recipe.yaml +3 -3
- package/recipes/cors-api/README.md +2 -2
- package/recipes/cors-api/recipe.yaml +2 -2
- package/recipes/health-page/README.md +1 -1
- package/recipes/health-page/recipe.yaml +1 -1
- package/recipes/json-api/README.md +1 -1
- package/recipes/json-api/recipe.yaml +4 -4
- package/recipes/middleware/README.md +8 -4
- package/recipes/middleware/middleware/bucket.mjs +12 -2
- package/recipes/middleware/middleware/locale.mjs +7 -3
- package/recipes/middleware/recipe.yaml +1 -1
- package/recipes/protected-download/README.md +1 -1
- package/recipes/protected-download/recipe.yaml +2 -2
- package/recipes/redirect/recipe.yaml +1 -1
- package/recipes/static-plus-api/README.md +2 -2
- package/recipes/static-plus-api/public/index.html +1 -1
- package/recipes/static-plus-api/recipe.yaml +2 -2
- package/recipes/static-plus-api/urlcode.yaml +1 -1
- package/recipes/typescript/README.md +2 -1
- package/recipes/typescript/recipe.yaml +5 -5
- package/recipes/webhook-receiver/README.md +5 -1
- package/recipes/webhook-receiver/recipe.yaml +2 -1
- package/recipes/webhook-receiver/urlcode.yaml +7 -0
- package/schemas/recipe.schema.json +4 -3
- package/schemas/urlcode.schema.json +14 -41
- package/skills/urlcode/SKILL.md +32 -11
- package/starters/default/AGENTS.md +18 -18
- package/starters/default/urlcode.yaml +0 -1
- package/dist/link-api.js +0 -136
- package/dist/link-cli.js +0 -141
- package/dist/link-events.js +0 -76
- package/dist/link-records.js +0 -31
- package/dist/link-store-worker.js +0 -150
- package/dist/link-store.js +0 -250
- package/dist/management-policy.js +0 -40
- package/dist/sqlite-version.js +0 -6
- package/dist/types/link-api.d.ts +0 -30
- package/dist/types/link-cli.d.ts +0 -37
- package/dist/types/link-events.d.ts +0 -27
- package/dist/types/link-records.d.ts +0 -11
- package/dist/types/link-store-worker.d.ts +0 -1
- package/dist/types/link-store.d.ts +0 -130
- package/dist/types/management-policy.d.ts +0 -8
- package/dist/types/sqlite-version.d.ts +0 -1
- package/docs/DYNAMIC-LINKS.md +0 -61
- package/docs/links/cli.md +0 -110
- package/docs/links/limits.md +0 -175
- package/docs/links/management-api.md +0 -80
- package/docs/links/pools.md +0 -75
- package/docs/links/setup.md +0 -135
- package/docs/yaml/links.md +0 -30
- package/examples/live-links/README.md +0 -11
- package/examples/live-links/example.yaml +0 -21
- package/examples/live-links/tests/requests.json +0 -6
- package/examples/live-links/urlcode.yaml +0 -16
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# Spike: layering `link` and `middleware` out of core
|
|
2
|
+
|
|
3
|
+
> Review update, 2026-09-19: Current baseline: **both extractions this spike
|
|
4
|
+
> proposes are over, and neither ended in a shipped separate package.** Link
|
|
5
|
+
> extraction completed and the resulting package was then retired. Middleware
|
|
6
|
+
> extraction never completed: core still implements native middleware, and the
|
|
7
|
+
> separate middleware extension — which added a different API with per-entry
|
|
8
|
+
> sandboxing — has itself been unpublished at `0.1.0-alpha.2` and its
|
|
9
|
+
> repository deleted. The extraction direction below is superseded, and it is
|
|
10
|
+
> not an instruction to remove core middleware: core's native `middleware:`
|
|
11
|
+
> array is now the only implementation of per-route middleware there is.
|
|
12
|
+
>
|
|
13
|
+
> An earlier version of this note said "monorepo work is starting now:
|
|
14
|
+
> middleware moves in as a separate package first, and folds into core
|
|
15
|
+
> afterward." Monorepo work is still starting, for core, auth, admin and UI —
|
|
16
|
+
> but the middleware clause is void. There is no package to move in and
|
|
17
|
+
> nothing to fold in afterward.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
Status: proposal, nothing implemented. No code in this repository does any of
|
|
21
|
+
this, and nothing here is committed scope. This combines two extraction ideas
|
|
22
|
+
into one spike because they share a mechanism and a sequence, not because
|
|
23
|
+
either is blocked on the other.
|
|
24
|
+
|
|
25
|
+
> **Update:** this spike's `link` extraction (Phase 1 below) was implemented —
|
|
26
|
+
> core no longer has a native `link`/`LinkStore`/`dynamicLinks` API — and the
|
|
27
|
+
> separate `urlcode-dynamic-link` package that received it has since been
|
|
28
|
+
> retired, unpublished from npm and its repository deleted (September 2026).
|
|
29
|
+
> Nothing owns stored links today. Present-tense references to `link` as a core
|
|
30
|
+
> feature elsewhere in this document describe the pre-extraction state this
|
|
31
|
+
> spike was analyzing, not current core.
|
|
32
|
+
|
|
33
|
+
## The shape this is aiming at
|
|
34
|
+
|
|
35
|
+
Core's job is to stay the smallest thing that is still a complete product on
|
|
36
|
+
its own: YAML routing (`redirect`/`respond`/`page`/`static`/`download`) plus
|
|
37
|
+
`function`, the one primitive with an actual serverless story
|
|
38
|
+
([`SPIKE-LAMBDA-COMPILE.md`](SPIKE-LAMBDA-COMPILE.md) — compile each `function`
|
|
39
|
+
route to its own Lambda, the pattern `src/build-cloudflare.ts` already uses).
|
|
40
|
+
That base alone serves static and dynamic content and deploys anywhere
|
|
41
|
+
(`docs/FRAMEWORK.md` layer 1–3). Everything past that — accounts, admin,
|
|
42
|
+
stored links, UI kit — is an extension a project opts into. `docs/FRAMEWORK.md`
|
|
43
|
+
already describes this as four composed packages; `auth` is the existing proof
|
|
44
|
+
that "basic project to enterprise in minutes" works by adding declarations,
|
|
45
|
+
not by forking core.
|
|
46
|
+
|
|
47
|
+
`link` and `middleware` are the two pieces of today's core that don't fit that
|
|
48
|
+
story: `link` owns durable state no other core feature needs, and
|
|
49
|
+
`middleware` is guest code that only some projects want. Extracting both
|
|
50
|
+
narrows core to exactly YAML + function, matching the vision above.
|
|
51
|
+
|
|
52
|
+
## Why one spike, not two independent ones
|
|
53
|
+
|
|
54
|
+
Both extractions land on the same extension mechanism, already implemented in
|
|
55
|
+
`src/extensions.ts`, and reusing one proof point for both keeps the design
|
|
56
|
+
honest instead of inventing two different shapes:
|
|
57
|
+
|
|
58
|
+
- **Mount ownership** (`route.extension`, `RuntimeExtension.activate` →
|
|
59
|
+
`ExtensionInstance.handle`): an extension owns an exclusive `/prefix/*` and
|
|
60
|
+
answers every request under it. This is the shape `link` needs — it already
|
|
61
|
+
behaves like a self-contained mount (documented in the since-retired
|
|
62
|
+
`urlcode-dynamic-link` package), and
|
|
63
|
+
`auth`/`admin` already use it for `/account/*` and `/admin/*`
|
|
64
|
+
(`docs/FRAMEWORK.md:10-15`).
|
|
65
|
+
- **Wrap without owning** (`ExtensionInstance.authorize`, called when a route
|
|
66
|
+
declares `policies.extensions.<name>` without an `extension:` mount): the
|
|
67
|
+
extension is asked to authorize a request that some *other* handler will
|
|
68
|
+
still serve, and can short-circuit with a `HandlerResult` or return nothing
|
|
69
|
+
(`src/extensions.ts:19`, `docs/EXTENSIONS.md:74-78`: "Its instance handles
|
|
70
|
+
bounded requests and, when used in policies, authorizes requests"). This is
|
|
71
|
+
already exactly how `auth` guards a `redirect`/`page`/`function` route today
|
|
72
|
+
without taking it over.
|
|
73
|
+
|
|
74
|
+
**Resolved — `ExtensionInstance.middleware` now exists.** This section
|
|
75
|
+
previously identified a real gap: `authorize()` can only gate (run before the
|
|
76
|
+
handler, either proceed or short-circuit) and never sees the handler's actual
|
|
77
|
+
response, so it could not express what native `middleware:` does today — wrap
|
|
78
|
+
the handler with `next()`, running code before *and* after it and
|
|
79
|
+
inspecting/mutating the returned `HandlerResult`. That gap is closed: a third,
|
|
80
|
+
additive `ExtensionInstance` capability, `middleware(config, request, next)`,
|
|
81
|
+
is attached the same way as `authorize` (`policies.extensions.<name>`, same
|
|
82
|
+
validated `config`) but with wrap semantics, chainable across multiple
|
|
83
|
+
extensions declared on one route, and composable with `authorize` on the same
|
|
84
|
+
route without either mechanism special-casing the other (see
|
|
85
|
+
[EXTENSIONS.md#wrapping-a-route-extension-middleware](EXTENSIONS.md#wrapping-a-route-extension-middleware)
|
|
86
|
+
and `src/extensions.ts`/`src/runtime.ts`). `urlcode-middleware` now has a real
|
|
87
|
+
contract to build the extraction against, rather than only mount ownership and
|
|
88
|
+
the gate-only `authorize()`.
|
|
89
|
+
|
|
90
|
+
**Resolved — `ExtensionActivation.root` now exists.** Building
|
|
91
|
+
`urlcode-middleware` against `ExtensionInstance.middleware` surfaced a second
|
|
92
|
+
gap: resolving a project-relative `source` (`middleware/headers.mjs`, the same
|
|
93
|
+
shape core's own native `middleware:` entries use) needs the project's
|
|
94
|
+
resolved directory, and `ExtensionActivation` (`{origin, target,
|
|
95
|
+
projectSha256, mounts}`) didn't carry one. `process.cwd()` is not a
|
|
96
|
+
substitute — `--project`/`--host-file` are independent, arbitrary paths, a
|
|
97
|
+
server can be started from any working directory, and the JS API can load a
|
|
98
|
+
project programmatically with no relationship to `cwd()` at all. `root` is
|
|
99
|
+
now a field on `ExtensionActivation`, set from the same resolved path
|
|
100
|
+
(`loadDocument()`'s `realpath`) that `router.ts`'s `functionFile()` already
|
|
101
|
+
resolves native `function`/`middleware` sources against, so an extension
|
|
102
|
+
resolves project-relative paths the identical way core does.
|
|
103
|
+
|
|
104
|
+
**Superseded by `docs/SPIKE-DEFAULT-TRUST-MODEL.md` — read that first.** This
|
|
105
|
+
section originally argued `middleware` should stay sandboxed like `function`
|
|
106
|
+
was under the old blanket-untrusted default. The maintainer has since decided
|
|
107
|
+
first-party `function`/`middleware` code is **trusted by default**, with
|
|
108
|
+
sandboxing an explicit per-declaration opt-in (`sandbox: true`), and
|
|
109
|
+
confirmed the same rule applies uniformly to `middleware` — no special case
|
|
110
|
+
for its wider per-request blast radius. So: `urlcode-middleware` runs trusted
|
|
111
|
+
in-process by default, same as `function`, with the sandboxed path available
|
|
112
|
+
for whichever specific `middleware:` wrap a developer judges needs it.
|
|
113
|
+
`authorize()` is still a distinct mechanism reserved for vetted,
|
|
114
|
+
operator-installed, revision-pinned extensions like `auth` — that split is
|
|
115
|
+
unchanged — but the reason `middleware` doesn't use `authorize()` is now
|
|
116
|
+
about mount-vs-wrap shape and contract ownership, not about needing its own
|
|
117
|
+
guest sandbox by default the way this section originally argued.
|
|
118
|
+
|
|
119
|
+
## Sequence: `link` first, `middleware` second — and why it isn't arbitrary
|
|
120
|
+
|
|
121
|
+
1. **`link` → `@jimhoyd/urlcode-dynamic-link`.** Already scoped in detail in
|
|
122
|
+
a sibling session's reviewed plan (Phase 1: remove `link`/`dynamicLinks`
|
|
123
|
+
from `src/types.ts`, `src/router.ts`, `src/runtime.ts`
|
|
124
|
+
(`src/runtime.ts:269-282`), schema and ~13 test files; Phase 2: the new
|
|
125
|
+
package, blocked on that repo existing/being attached). It is ready to
|
|
126
|
+
execute pending approval and needs nothing from the middleware work to
|
|
127
|
+
proceed.
|
|
128
|
+
2. **`middleware` → extension.** Depends on Phase 1's incidental fix, not on
|
|
129
|
+
Phase 2: today `src/capabilities.ts` gives a blanket native/refused
|
|
130
|
+
answer for `extension`/`policies.extensions` without consulting the
|
|
131
|
+
specific registered extension's own `targets`
|
|
132
|
+
(`RuntimeExtension.targets`, `src/extensions.ts:31`) — wrong for any
|
|
133
|
+
self-hosted-only extension. `link`'s Phase 1c makes capability analysis
|
|
134
|
+
extension-target-aware. A `middleware` extension needs that same fix to
|
|
135
|
+
correctly report itself refused on Cloudflare/Vercel/AWS the way today's
|
|
136
|
+
native `middleware` handler is refused there — so it should land after,
|
|
137
|
+
reusing that work rather than duplicating it.
|
|
138
|
+
|
|
139
|
+
Sequence matters for that one dependency; nothing else forces an order.
|
|
140
|
+
`middleware` extraction now also depends on `docs/SPIKE-DEFAULT-TRUST-MODEL.md`
|
|
141
|
+
landing first (trusted-by-default needs to exist as a real execution path
|
|
142
|
+
before `urlcode-middleware` can be built against it) — recommended order is
|
|
143
|
+
`link` → default-trust-model → `middleware`, not `link` → `middleware`.
|
|
144
|
+
|
|
145
|
+
## Cross-repo dependency
|
|
146
|
+
|
|
147
|
+
Neither extraction is core-only in effect, even though Phase 1 of each is
|
|
148
|
+
core-only in *scope*. `docs/FRAMEWORK.md:10-15` lists three other repos —
|
|
149
|
+
`urlcode-auth`, `urlcode-admin`, `urlcode-ui` — that already implement
|
|
150
|
+
`RuntimeExtension`/`ExtensionInstance` against core's contract
|
|
151
|
+
(`src/extensions.ts`). None of them are attached to this session, so the
|
|
152
|
+
following is reasoned from the documented contract, not verified against
|
|
153
|
+
their actual source; it needs confirming against those repos (via `add_repo`)
|
|
154
|
+
before anything here is treated as settled.
|
|
155
|
+
|
|
156
|
+
- **Core is upstream of every extension repo, never the reverse.** `auth`,
|
|
157
|
+
`admin`, `ui`, and the future `dynamic-link` and `middleware` extensions
|
|
158
|
+
each pin to a core contract version; core does not import or depend on any
|
|
159
|
+
of them (`AGENTS.md`: "Core never imports them"). So the dependency
|
|
160
|
+
direction for both extractions is: land the core contract change and cut a
|
|
161
|
+
release, *then* update/ship the consuming extension repos against it — not
|
|
162
|
+
the other way around.
|
|
163
|
+
- **The capability-analysis fix (`link`'s Phase 1c) is additive, not a
|
|
164
|
+
`RuntimeExtension` contract change.** It changes what core *reports* about
|
|
165
|
+
an extension's declared `targets`, not the shape an extension implements.
|
|
166
|
+
On paper this needs no changes in `auth`/`admin`/`ui` — but that assumption
|
|
167
|
+
should be checked against their actual `targets` declarations once those
|
|
168
|
+
repos are available, since a repo currently relying on the old blanket
|
|
169
|
+
native/refused answer could see a new, more accurate `refused` result it
|
|
170
|
+
wasn't expecting.
|
|
171
|
+
- **`middleware` does not reuse `authorize()`, so it does not force a change
|
|
172
|
+
to that hook** — it needs its own contract surface (a sandboxed-execution
|
|
173
|
+
extension shape, closer to how `function` is wired in `runtime.ts` than to
|
|
174
|
+
how `auth` is wired). `auth`/`admin`/`ui` are unaffected by `middleware`'s
|
|
175
|
+
extraction unless a project happens to combine `middleware` with one of
|
|
176
|
+
them, which is a project-level composition question, not a contract change
|
|
177
|
+
those three repos need to absorb.
|
|
178
|
+
- **A new `urlcode-dynamic-link` and a new `urlcode-middleware` repo** both
|
|
179
|
+
need to exist or be attached before their Phase 2 work can be written or
|
|
180
|
+
verified, matching the constraint already flagged for `link`.
|
|
181
|
+
|
|
182
|
+
## Repo governance for the two new repos (decided — both repos since deleted)
|
|
183
|
+
|
|
184
|
+
> **Historical, 2026-09-19.** Both repositories this section governs were
|
|
185
|
+
> created, released once, and then deleted; both packages are unpublished. The
|
|
186
|
+
> decisions below were applied while they existed and are kept as the recorded
|
|
187
|
+
> template for any future extension repository — not as a description of
|
|
188
|
+
> anything live. Note in particular that the "published public from the start"
|
|
189
|
+
> departure recorded below is the one whose consequences are worth reading
|
|
190
|
+
> back: both repos that took it are gone within days of their first release.
|
|
191
|
+
|
|
192
|
+
Both `urlcode-dynamic-link` and `urlcode-middleware` follow `GOVERNANCE.md`
|
|
193
|
+
and `AGENTS.md` as written, with one explicit decision recorded here per
|
|
194
|
+
AGENTS.md's "do not publish packages without an explicit decision":
|
|
195
|
+
|
|
196
|
+
- **License: Apache-2.0**, same as core, no separate CLA/DCO — matching
|
|
197
|
+
`GOVERNANCE.md`'s "Licensing and participation" section exactly. No new
|
|
198
|
+
licensing terms for either repo.
|
|
199
|
+
- **Repo settings mirror core's ruleset** (`GOVERNANCE.md` "Changes and
|
|
200
|
+
responsibility"): `main` protected against force-push/deletion, requires an
|
|
201
|
+
up-to-date branch, passing CI and a PR, squash merges, no ruleset bypass for
|
|
202
|
+
admins or automation, CODEOWNERS recording ownership. CI/release workflow
|
|
203
|
+
shape copied from core's `release.yml` (candidate build → audit → pack →
|
|
204
|
+
attest → publish via trusted publisher, no long-lived npm token), per the
|
|
205
|
+
pattern §2.1 of the archived plan (`docs/archive/2026-09-19/NEXT-STEPS.md`)
|
|
206
|
+
already used for `auth`/`admin`/`ui`.
|
|
207
|
+
CodeQL required on main, secret scanning and push protection on, same as
|
|
208
|
+
core.
|
|
209
|
+
- **Published public from the start** — both the GitHub repo and the npm
|
|
210
|
+
package (`@jimhoyd/urlcode-dynamic-link`, `@jimhoyd/urlcode-middleware`) are
|
|
211
|
+
public, not the "`private: true` until reviewed" alpha pattern
|
|
212
|
+
`auth`/`admin`/`ui` used at their first release. This is a deliberate
|
|
213
|
+
departure from that precedent, not an oversight — record the same alpha
|
|
214
|
+
caveat in each README/status file (source complete, independent review and
|
|
215
|
+
deployment evidence pending) so "public" doesn't read as "reviewed."
|
|
216
|
+
- Naming matches convention: repo `urlcode-<name>` ↔ package
|
|
217
|
+
`@jimhoyd/urlcode-<name>`, consistent with `urlcode-auth`/`-admin`/`-ui`.
|
|
218
|
+
- Still outside this session's scope to execute: creating the two GitHub
|
|
219
|
+
repos, setting their branch protection/CODEOWNERS, and the actual npm
|
|
220
|
+
publish are maintainer actions, not something done from within this repo's
|
|
221
|
+
checkout.
|
|
222
|
+
|
|
223
|
+
## Performance considerations
|
|
224
|
+
|
|
225
|
+
Both extractions keep everything in the same Node process — extensions are
|
|
226
|
+
loaded and activated in-process via a host file (`src/extensions.ts`), not a
|
|
227
|
+
network hop or separate deployment unit — so neither is a "distributed
|
|
228
|
+
system tax." The real costs are narrower and different for each:
|
|
229
|
+
|
|
230
|
+
- **`link`** moving from a native `runtime.ts` branch (`src/runtime.ts:269-282`)
|
|
231
|
+
to an extension mount means every stored-link lookup now also passes through
|
|
232
|
+
`extensionResponse()` (`src/extensions.ts:164-172`): a header-count/byte-size
|
|
233
|
+
check (≤256 headers, ≤16 KiB), a 1 MiB body-size assert, and a `Cache-Control`
|
|
234
|
+
rewrite. That's small, bounded, per-request work — but `link` is the
|
|
235
|
+
project's most latency-sensitive path (a redirect lookup), and
|
|
236
|
+
`docs/CAPACITY.md:200` already warns "do not extrapolate in-memory redirect
|
|
237
|
+
benchmark numbers to database lookups" for the *native* handler today. The
|
|
238
|
+
extension path adds a fixed increment on top of that existing SQLite-bound
|
|
239
|
+
latency; worth a benchmark comparison (native vs. extension-mounted `link`)
|
|
240
|
+
before calling this cost-neutral rather than assuming it from the code shape.
|
|
241
|
+
- **`middleware`'s performance story changes under `SPIKE-DEFAULT-TRUST-MODEL.md`.**
|
|
242
|
+
Under the old blanket-sandboxed default, this section argued middleware's
|
|
243
|
+
cost was worse than `function`'s because it runs on every request through
|
|
244
|
+
every route it's attached to, not once per matched route — paying the
|
|
245
|
+
worker-thread/fresh-heap tax repeatedly on the hot path. That cost is now
|
|
246
|
+
the *opt-in* path, not the default: trusted-by-default execution runs
|
|
247
|
+
`middleware` as ordinary in-process code with no worker pool ceiling, the
|
|
248
|
+
same throughput profile `authorize()` already has (see the concurrency
|
|
249
|
+
discussion in this conversation — thousands of concurrent trusted
|
|
250
|
+
executions is realistic, bounded by normal Node concurrency and the
|
|
251
|
+
instance's HTTP admission cap, not a fixed worker-slot count). The sandbox
|
|
252
|
+
cost described above still applies, in full, to whichever specific
|
|
253
|
+
`middleware:` wrap a developer explicitly opts into `sandbox: true` — and
|
|
254
|
+
the same mitigation still matters there: native declarative primitives
|
|
255
|
+
(the Phase 4.2 `auth: { required: true, role: ... }` direction in the
|
|
256
|
+
archived plan, `docs/archive/2026-09-19/NEXT-STEPS.md`) reduce how much logic
|
|
257
|
+
ever needs to reach for the sandboxed opt-in at all, for the cases where a
|
|
258
|
+
developer does judge it warranted.
|
|
259
|
+
|
|
260
|
+
## Other core pieces considered and set aside
|
|
261
|
+
|
|
262
|
+
Checked against the same test used for `link`/`middleware` — does it own
|
|
263
|
+
state or behavior nothing else in core needs, and is it optional rather than
|
|
264
|
+
part of the smallest complete product:
|
|
265
|
+
|
|
266
|
+
- **`proxy`** — explicitly *not* a candidate. The sibling session's plan for
|
|
267
|
+
`link` calls this out directly: unlike `link`, `proxy` is a shared egress
|
|
268
|
+
primitive future extensions are expected to build on, so extracting it
|
|
269
|
+
would create a dependency extensions have on an extension, which core's
|
|
270
|
+
"extensions never depend on each other" shape doesn't support today.
|
|
271
|
+
- **`policies`** (`throttle`, `agents`, security headers, compression,
|
|
272
|
+
cache) — these are declarative YAML behavior applied by core to every
|
|
273
|
+
route, not guest code or durable external state; `throttle`/`agents`
|
|
274
|
+
counters are already scoped as "per instance, not distributed"
|
|
275
|
+
(`docs/OPERATIONS.md`), which is a limitation to document, not a reason to
|
|
276
|
+
extract. A bare project (no extensions at all) still needs security
|
|
277
|
+
headers and basic rate limiting, so these stay part of the smallest
|
|
278
|
+
complete product.
|
|
279
|
+
- **`conditional`, `static`, `download`, `page`, `respond`, `redirect`** —
|
|
280
|
+
these *are* the YAML-routing half of "YAML + function"; extracting any of
|
|
281
|
+
them would shrink core below the "complete product on its own" bar rather
|
|
282
|
+
than trim it.
|
|
283
|
+
- **Management API / operator grants / credential policy** — foundation that
|
|
284
|
+
extensions themselves depend on (`docs/archive/2026-09-19/MANAGEMENT-SECURITY.md`,
|
|
285
|
+
`docs/FUNCTION-SECURITY.md`); moving it out would mean extracting the thing
|
|
286
|
+
the extraction pattern relies on.
|
|
287
|
+
|
|
288
|
+
Nothing else in core matches the `link`/`middleware` shape today. If a third
|
|
289
|
+
candidate is going to be found, the repetition-log discipline of Phase 6 in the
|
|
290
|
+
archived plan (`docs/archive/2026-09-19/NEXT-STEPS.md`) — extract from observed
|
|
291
|
+
repetition, not speculation — is the more defensible way to find it than
|
|
292
|
+
continuing to eyeball the handler list. That plan records Phase 6 as never
|
|
293
|
+
started, and no `docs/REPETITION-LOG.md` was ever written, so the log itself
|
|
294
|
+
would have to be started before it could inform the decision.
|
|
295
|
+
|
|
296
|
+
Recommendation: before either Phase 2 begins, attach `urlcode-auth`,
|
|
297
|
+
`urlcode-admin` and `urlcode-ui` to a session and confirm (a) their actual
|
|
298
|
+
`targets` declarations against the Phase 1c capability-analysis change, and
|
|
299
|
+
(b) whether any of them implement `authorize()` beyond `auth`. That turns the
|
|
300
|
+
bullets above from reasoned-from-docs into verified, and gives real basis for
|
|
301
|
+
sequencing core's release against theirs (e.g. a core minor version that adds
|
|
302
|
+
extension-target-aware capability reporting without breaking the contract,
|
|
303
|
+
versus a core change that requires those repos to update in lockstep).
|
|
304
|
+
|
|
305
|
+
## Open questions before either is built
|
|
306
|
+
|
|
307
|
+
- `link`: hard break vs. deprecation window (pre-1.0, `0.4.0-alpha.1`;
|
|
308
|
+
sibling session's plan recommends a hard break, flagged explicitly since it
|
|
309
|
+
breaks any project with `dynamicLinks: true` until the new package ships).
|
|
310
|
+
- `middleware`: how much of today's `middleware:` guest code is genuinely
|
|
311
|
+
custom logic versus a common pattern (auth gating, headers, CORS, rate
|
|
312
|
+
limiting) that a native declarative primitive could cover with no guest
|
|
313
|
+
execution at all? This sizing decides how much the sandbox's per-request
|
|
314
|
+
cost actually matters in practice, and should come from an inventory of
|
|
315
|
+
real usage, not a guess, before `urlcode-middleware`'s guest-execution
|
|
316
|
+
contract is designed.
|
|
317
|
+
- Both: this repo stays core-only per `AGENTS.md` ("the auth, admin and ui
|
|
318
|
+
extensions live in their own repositories... Core never imports them");
|
|
319
|
+
neither extraction's Phase 2 can be written here.
|
|
320
|
+
|
|
321
|
+
## The full ladder: one contract, one vocabulary per level
|
|
322
|
+
|
|
323
|
+
`link` and `middleware` shrink core by moving pieces *out*; there's a
|
|
324
|
+
complementary, additive move that extends the ladder *below* core instead of
|
|
325
|
+
touching it: a `static` compile target, alongside the existing
|
|
326
|
+
`node`/`aws`/`vercel`/`cloudflare` targets in `src/capabilities.ts`. Same
|
|
327
|
+
`urlcode.yaml`, same routing vocabulary — the difference between levels is
|
|
328
|
+
only which capabilities a given target can serve, exactly the mechanism that
|
|
329
|
+
already exists (Cloudflare already refuses `function`/`link`/`middleware`
|
|
330
|
+
today; `static` would additionally refuse `function`, keeping only
|
|
331
|
+
`redirect`/`respond`/`page`/`static`/`download`). No new syntax, no second
|
|
332
|
+
schema, no fork of the contract — a project written once reads as:
|
|
333
|
+
|
|
334
|
+
```
|
|
335
|
+
static hosting (S3, CloudFront) → routing + static assets only, no server
|
|
336
|
+
node/aws/vercel (serverless) → + function, the dynamic primitive
|
|
337
|
+
extensions (auth/admin/link/…) → + accounts, admin, stored links, middleware
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
This is the same YAML at every level; the only thing that changes is which
|
|
341
|
+
handlers a target accepts, reported the same way `urlcode capabilities
|
|
342
|
+
--target <name>` already reports it. That's the point being made here: the
|
|
343
|
+
progression isn't three different products, it's one contract with graduated
|
|
344
|
+
vocabulary, so a project can start at "static site" and grow into "function"
|
|
345
|
+
and then "extensions" without a rewrite — just fewer refusals as the target
|
|
346
|
+
gets more capable.
|
|
347
|
+
|
|
348
|
+
This is additive, not part of the `link`/`middleware` extraction: it doesn't
|
|
349
|
+
touch core's code, doesn't shrink core's self-definition ("YAML + function"
|
|
350
|
+
stays true for the `node`/`aws`/`vercel` targets), and needs nothing from
|
|
351
|
+
either extraction to be built. It reuses `build-cloudflare.ts`'s pattern
|
|
352
|
+
(compile YAML to the target's native format) for S3/CloudFront redirect
|
|
353
|
+
rules and object routing.
|
|
354
|
+
|
|
355
|
+
**One real gap, not glossed over:** GitHub Pages has no server-side rewrite
|
|
356
|
+
layer, so `redirect` routes can't compile to true HTTP redirects there — only
|
|
357
|
+
a meta-refresh/JS fallback or a static 404-page trick, both lower fidelity
|
|
358
|
+
than what the same route does on every other target. If `static` ships,
|
|
359
|
+
GitHub Pages needs either an explicit fidelity caveat in its target
|
|
360
|
+
description or exclusion from the `static` target's claimed support, not a
|
|
361
|
+
silent "same behavior everywhere" promise the platform can't keep.
|
|
362
|
+
|
|
363
|
+
## Non-goals
|
|
364
|
+
|
|
365
|
+
This spike does not decide `link`'s Phase 1 breaking-change policy, does not
|
|
366
|
+
design the `middleware` extension's config schema, and does not touch
|
|
367
|
+
`function` — `function` stays in core as the serverless-capable primitive
|
|
368
|
+
that makes YAML + function a complete product on its own.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Decision: first-party `function`/`middleware` code is trusted by default
|
|
2
|
+
|
|
3
|
+
Status: **decided by the maintainer and implemented in `0.4.0-alpha.2`**
|
|
4
|
+
(`sandbox`/`sandboxReason` in `schemas/urlcode.schema.json`; see the
|
|
5
|
+
`0.4.0-alpha.2` entry in [the roadmap](../ROADMAP.md) and the resulting contract
|
|
6
|
+
in [function security](FUNCTION-SECURITY.md)). The release is published; this file retains the decision rationale. This overturns an
|
|
7
|
+
explicit, previously-stated project rule — see "What this reverses" below —
|
|
8
|
+
so treat it as a deliberate, recorded policy change, not a code cleanup. The
|
|
9
|
+
body below is preserved as written at decision time; where it calls downstream
|
|
10
|
+
packages unbuilt, that was true then, and the notes mark what exists now.
|
|
11
|
+
|
|
12
|
+
## The decision
|
|
13
|
+
|
|
14
|
+
Project-authored `function` and `middleware` code runs **trusted and
|
|
15
|
+
unsandboxed by default** (direct host-process execution, no worker thread, no
|
|
16
|
+
QuickJS/WASM, no fresh-heap-per-call cost). Sandboxing becomes **opt-in**,
|
|
17
|
+
declared per route by the developer when they judge a specific piece of code
|
|
18
|
+
needs it (e.g. code processing input from an untrusted third party, a
|
|
19
|
+
plugin/contribution the project owner hasn't personally reviewed, or genuinely
|
|
20
|
+
adversarial-input-facing logic).
|
|
21
|
+
|
|
22
|
+
Rationale on record: the maintainer's own reasoning is that the current
|
|
23
|
+
blanket sandbox is a major performance ceiling (2 workers, no queue, shared
|
|
24
|
+
across every programmable route — `docs/CAPACITY.md`) that does not scale to
|
|
25
|
+
real concurrent traffic "out of the box," and that treating all first-party
|
|
26
|
+
code as equally untrusted regardless of the developer's own judgment is the
|
|
27
|
+
wrong default for a framework whose primary author is often the same person
|
|
28
|
+
deploying it. The alternative (sandboxed-by-default, opt-in-to-trusted) was
|
|
29
|
+
raised and explicitly rejected in favor of this direction.
|
|
30
|
+
|
|
31
|
+
Industry comparison supporting this call: mainstream frameworks that serve
|
|
32
|
+
AI-agent-generated backend code today (Express, Next.js API routes, Django,
|
|
33
|
+
Rails) run that code directly in the host process at serve time, unsandboxed,
|
|
34
|
+
identically to hand-written code — there is no mainstream precedent for
|
|
35
|
+
production request-handling code running in an isolated engine. Where the
|
|
36
|
+
industry *does* sandbox AI-generated code (E2B, Modal sandboxes, OpenAI Code
|
|
37
|
+
Interpreter, Anthropic's own code execution tool) is at generation/dev-time,
|
|
38
|
+
while the agent is writing and iterating — not at serve-time once code is
|
|
39
|
+
reviewed and deployed. URLCode's current design is the unusual one: it keeps
|
|
40
|
+
the isolation boundary at production serve-time rather than following the
|
|
41
|
+
"sandbox generation, trust deployment" pattern the rest of the ecosystem
|
|
42
|
+
uses. This decision brings first-party `function`/`middleware` code in line
|
|
43
|
+
with that mainstream pattern by default, while keeping the stricter,
|
|
44
|
+
less-common serve-time isolation available as an explicit opt-in for code
|
|
45
|
+
that specifically warrants it.
|
|
46
|
+
|
|
47
|
+
## What this reverses — read before touching anything else
|
|
48
|
+
|
|
49
|
+
This is not additive; it contradicts explicit, multiple-file statements that
|
|
50
|
+
currently describe the opposite as an unconditional guarantee. All of the
|
|
51
|
+
following assert or depend on "guest code is always sandboxed, regardless of
|
|
52
|
+
origin, even your own repository," and need to be rewritten, not just
|
|
53
|
+
extended, once this ships:
|
|
54
|
+
|
|
55
|
+
- `AGENTS.md:9-11` — "Treat all application code as untrusted... never add a
|
|
56
|
+
host-code execution fallback."
|
|
57
|
+
- `docs/FUNCTION-SECURITY.md:3-6` — "Application code is untrusted even when
|
|
58
|
+
it came from your own Git repository... There is no `unsafe`, `trusted` or
|
|
59
|
+
automatic host-execution fallback." (This line directly anticipated and
|
|
60
|
+
rejected exactly this proposal; it cannot be left standing as written.)
|
|
61
|
+
- `SECURITY.md:3-5` — "Application functions are untrusted by default...
|
|
62
|
+
There is no unrestricted host-execution fallback."
|
|
63
|
+
- `docs/SANDBOX-REVIEW.md:5-6` — "No unrestricted Node execution fallback is
|
|
64
|
+
permitted."
|
|
65
|
+
- `docs/SECURITY-AUDIT.md` and `docs/PROJECT-DIRECTION.md:25-27` — the
|
|
66
|
+
"not a general Node web framework" framing and residual-risk reasoning
|
|
67
|
+
assume any guest code could be adversarial.
|
|
68
|
+
- `README.md`'s function description states the sandbox as an unconditional
|
|
69
|
+
runtime property, not a mode a route opts into.
|
|
70
|
+
- `docs/AI-AUTHORING.md` treats `FUNCTION-SECURITY.md` as required reading
|
|
71
|
+
with no first-party exemption in the authoring contract.
|
|
72
|
+
|
|
73
|
+
None of these can be quietly left in place once `trusted` is the default —
|
|
74
|
+
an operator or a security reviewer reading them after this ships would be
|
|
75
|
+
reading claims the runtime no longer makes.
|
|
76
|
+
|
|
77
|
+
## What has to actually change (not just docs)
|
|
78
|
+
|
|
79
|
+
There is no execution branch for this today — sandboxing is hardwired, not a
|
|
80
|
+
config path. Per the earlier sweep:
|
|
81
|
+
|
|
82
|
+
1. **Schema/types**: a per-route (or per-function-declaration) field, e.g.
|
|
83
|
+
`sandbox: true`, defaulting to `false`/absent = trusted. Needs a home in
|
|
84
|
+
`schemas/urlcode.schema.json` and `src/types.ts` (`RouteConfig`, whatever
|
|
85
|
+
shape `function`/`middleware` declarations take).
|
|
86
|
+
2. **Execution path**: `src/functions.ts`/`function-worker.ts` currently has
|
|
87
|
+
no branch that skips `FunctionPool`'s worker/QuickJS dispatch and calls a
|
|
88
|
+
guest export directly in-process — that in-process path needs to be built
|
|
89
|
+
from scratch as the new default, with the existing sandboxed path kept
|
|
90
|
+
fully intact and reachable via the opt-in flag.
|
|
91
|
+
3. **Trust-declaration integrity**: since this is opt-out rather than
|
|
92
|
+
opt-in, the risk shifts from "can code fake being trusted" (the sandboxed
|
|
93
|
+
default's concern) to "does everyone correctly opt untrusted-input-facing
|
|
94
|
+
code INTO the sandbox." That's a documentation/authoring-guidance problem,
|
|
95
|
+
not an enforcement one — `urlcode audit`/`validate` cannot know a
|
|
96
|
+
developer's intent, so the authoring docs (`docs/AI-AUTHORING.md`, the
|
|
97
|
+
generated project `AGENTS.md`, the `urlcode-authoring` skill) need to
|
|
98
|
+
clearly teach **when** a project should reach for `sandbox: true` — e.g.
|
|
99
|
+
code parsing third-party webhook payloads it doesn't fully trust, a
|
|
100
|
+
contributed function nobody on the team reviewed, anything handling a
|
|
101
|
+
secret binding it can't fully vet. Reasonable defaults in generated
|
|
102
|
+
scaffolding (recipes, starter templates) should still model this judgment
|
|
103
|
+
correctly rather than silently omitting it everywhere.
|
|
104
|
+
4. **Test suite**: `test/sandbox.test.ts`, `test/sandbox-pool.test.ts`,
|
|
105
|
+
`test/egress.test.ts`, `test/middleware.test.ts` and related isolation
|
|
106
|
+
suites (~60-90 tests) stay valid for the `sandbox: true` path unchanged;
|
|
107
|
+
new tests are needed for the trusted default path (it can reach Node APIs,
|
|
108
|
+
the module graph, etc., on purpose) plus tests confirming the two paths
|
|
109
|
+
don't cross-contaminate (a trusted-path failure can't be mistaken for a
|
|
110
|
+
sandboxed one, and vice versa).
|
|
111
|
+
5. **Capacity docs**: `docs/CAPACITY.md`'s worker/deadline/heap numbers stay
|
|
112
|
+
as the sandboxed-path limits; the trusted path needs its own documented
|
|
113
|
+
capacity model (ordinary Node concurrency, the existing HTTP admission cap
|
|
114
|
+
`--max-in-flight`, no worker-pool ceiling) — see the concurrency
|
|
115
|
+
discussion earlier in this conversation for the concrete numbers.
|
|
116
|
+
6. **Extension-model consistency**: `auth`/`admin`/`ui` already run trusted
|
|
117
|
+
via a *different* mechanism (`authorize()`/`handle()`, operator-installed,
|
|
118
|
+
revision-pinned packages outside the project). This decision does not
|
|
119
|
+
merge that model with first-party `function`/`middleware` trust — they
|
|
120
|
+
remain two separate trust paths that happen to both be unsandboxed, for
|
|
121
|
+
different reasons (operator-vetted package vs. developer's own judgment
|
|
122
|
+
call). Keep that distinction explicit in the docs rewrite so "trusted"
|
|
123
|
+
doesn't become one undifferentiated concept.
|
|
124
|
+
|
|
125
|
+
## Cross-repo impact
|
|
126
|
+
|
|
127
|
+
Same caveat as `docs/SPIKE-CORE-LAYERING.md`: `urlcode-auth`, `urlcode-admin`,
|
|
128
|
+
`urlcode-ui` are not attached to this session, so the following is reasoned
|
|
129
|
+
from the documented contract, not verified against their source — confirm
|
|
130
|
+
with `add_repo` before treating it as settled.
|
|
131
|
+
|
|
132
|
+
- **`auth`/`admin`/`ui` themselves: contract unaffected.** They run through
|
|
133
|
+
`authorize()`/`handle()` — a separate mechanism from `function`/`middleware`
|
|
134
|
+
guest execution, unchanged by this decision. No `RuntimeExtension` code
|
|
135
|
+
changes needed on their side.
|
|
136
|
+
- **But their own docs/examples likely need an audit anyway.** If any of the
|
|
137
|
+
three repos' READMEs, threat-model docs, or example projects describe core's
|
|
138
|
+
sandbox as an unconditional guarantee (language inherited from today's
|
|
139
|
+
`AGENTS.md`/`FUNCTION-SECURITY.md` wording, which this decision replaces),
|
|
140
|
+
those statements become inaccurate the moment core ships the new default.
|
|
141
|
+
Anywhere one of these repos ships an example `urlcode.yaml` with `function:`/
|
|
142
|
+
`middleware:` routes (onboarding docs, a demo, a conformance fixture), it's
|
|
143
|
+
now running trusted by default unless that example explicitly opts into
|
|
144
|
+
`sandbox: true` — worth checking whether any existing example implicitly
|
|
145
|
+
depends on sandboxing (e.g., a demo built to showcase "safe to run
|
|
146
|
+
arbitrary/third-party code") and would silently stop meaning what it claims.
|
|
147
|
+
- **This is a behavior change, not just a schema addition — versioning must
|
|
148
|
+
reflect that.** A project upgrading core with zero YAML changes gets a
|
|
149
|
+
materially different execution model for every existing `function`/
|
|
150
|
+
`middleware` route it already has. That needs a major/minor version bump
|
|
151
|
+
with an explicit changelog entry and migration note (not a patch release),
|
|
152
|
+
and `peers.json` in `auth`/`admin`/`ui` (`docs/SPIKE-CORE-LAYERING.md`'s
|
|
153
|
+
§2.2 reference) should pin deliberately to a core version that includes
|
|
154
|
+
this change, not inherit it silently on a routine bump.
|
|
155
|
+
- **`urlcode-dynamic-link` (built and published after this was written, then retired, unpublished and deleted in September 2026): moot.** It was a
|
|
156
|
+
mount-based extension like `auth`, not a `function`/`middleware` consumer —
|
|
157
|
+
nothing here changes its design.
|
|
158
|
+
- **`urlcode-middleware` (built and published since, then retired, unpublished
|
|
159
|
+
and deleted in September 2026): the rule outlived the package — decided.**
|
|
160
|
+
The decision below was made for that package but is not about it: it governs
|
|
161
|
+
core's own native `middleware:` array, which is where per-route middleware
|
|
162
|
+
lives now that the extension package is gone. First-party middleware is trusted by default, exactly like
|
|
163
|
+
`function`; `sandbox: true` is the same opt-in a developer reaches for
|
|
164
|
+
when a specific `middleware:` wrap genuinely warrants it (e.g. it's
|
|
165
|
+
processing input from a source the developer doesn't fully trust). This
|
|
166
|
+
was flagged as an open fork (middleware's wider blast radius — it wraps
|
|
167
|
+
every request through a route, not one operation) and the maintainer has
|
|
168
|
+
resolved it: one uniform default across `function` and `middleware`, not a
|
|
169
|
+
special case. `docs/SPIKE-CORE-LAYERING.md`'s middleware section, which
|
|
170
|
+
still describes middleware as sandboxed-by-default, is superseded by this
|
|
171
|
+
and needs updating to match.
|
|
172
|
+
- **Extension-authored project-level lifecycle hooks: same rule, no special
|
|
173
|
+
case.** `docs/EXTENSIONS.md`'s "Project-level lifecycle hooks" section
|
|
174
|
+
settles this explicitly: a hook a project names in an extension's own YAML
|
|
175
|
+
config (`urlcode-auth`'s `onSignUp`/`beforeRegister`/`onDelete` and the
|
|
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.
|
|
190
|
+
|
|
191
|
+
## Recommended sequencing
|
|
192
|
+
|
|
193
|
+
This is independent of, but touches the same files as, the `link`/
|
|
194
|
+
`middleware` extraction in `docs/SPIKE-CORE-LAYERING.md`. Recommend landing
|
|
195
|
+
this trust-model change first, since it changes what "keep middleware
|
|
196
|
+
sandboxed" in that spike even means (middleware's default execution mode
|
|
197
|
+
changes too) — building the extraction against the old assumption first
|
|
198
|
+
would mean redoing it once this ships. `docs/SPIKE-CORE-LAYERING.md`'s
|
|
199
|
+
middleware section will need a follow-up pass once this decision's schema
|
|
200
|
+
shape exists.
|
|
201
|
+
|
|
202
|
+
## Not decided here
|
|
203
|
+
|
|
204
|
+
- The exact field name/shape (`sandbox: true` vs. `trust: sandboxed` vs.
|
|
205
|
+
something else) — a naming/schema-design pass, not a policy question.
|
|
206
|
+
- Whether `link`'s extraction or the `static` target need any changes as a
|
|
207
|
+
result — on current understanding, no (neither touches guest-code
|
|
208
|
+
execution), but worth re-checking once the schema shape is settled.
|
|
209
|
+
- The actual rewritten wording for `AGENTS.md`/`SECURITY.md`/
|
|
210
|
+
`FUNCTION-SECURITY.md` — drafting that is a deliberate writing pass, not
|
|
211
|
+
something to improvise inline here.
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Spike: compiling `function` routes into their own Lambdas
|
|
2
2
|
|
|
3
|
+
> Review update, 2026-09-19: Current baseline: trusted Node execution is the default, so fresh invocation
|
|
4
|
+
> state is a guarantee of `sandbox: true` only. AWS/Vercel still reject functions
|
|
5
|
+
> 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.
|
|
9
|
+
|
|
10
|
+
|
|
3
11
|
Status: proposal, nothing implemented. No code in this repository does any of
|
|
4
12
|
this, and nothing here is committed scope.
|
|
5
13
|
|
|
@@ -109,8 +117,10 @@ out rather than picking one under time pressure.
|
|
|
109
117
|
|
|
110
118
|
Stored live links need a durable writable store that instances share. That is
|
|
111
119
|
the same refusal before and after this change. DynamoDB is the natural lowering,
|
|
112
|
-
but it is a store implementation with its own export and restore discipline
|
|
113
|
-
|
|
120
|
+
but it is a store implementation with its own export and restore discipline, not
|
|
121
|
+
something a compile step produces. (Note: the native `link` handler this
|
|
122
|
+
section describes was later removed from core; see
|
|
123
|
+
`docs/SPIKE-CORE-LAYERING.md`.)
|
|
114
124
|
|
|
115
125
|
## 4. Emitting infrastructure is a new kind of output
|
|
116
126
|
|
package/docs/STARTERS.md
CHANGED
|
@@ -44,7 +44,7 @@ See [readiness](READINESS.md) and [security](FUNCTION-SECURITY.md).
|
|
|
44
44
|
|
|
45
45
|
To start an extended site instead, install the extension packages from npm
|
|
46
46
|
(`npm install @jimhoyd/urlcode-auth @jimhoyd/urlcode-admin`, published as
|
|
47
|
-
`0.1.0-alpha.
|
|
47
|
+
`0.1.0-alpha.x` prereleases) in the directory you run from and pass their names: `urlcode init ../my-site --with
|
|
48
48
|
auth,admin` writes the same starter under `my-site/app/`, merges each package's
|
|
49
49
|
routes and declarations into it, and generates one `host.mjs` and README beside
|
|
50
50
|
it. The contract each package fulfils is in [extensions](EXTENSIONS.md#scaffolding-with-init---with).
|