@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.
Files changed (232) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +36 -22
  2. package/.claude/skills/urlcode-operations/SKILL.md +16 -22
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CONTRIBUTING.md +20 -16
  5. package/README.md +83 -80
  6. package/ROADMAP.md +61 -331
  7. package/SECURITY.md +23 -8
  8. package/dist/BUILD-MANIFEST.json +42 -47
  9. package/dist/agents-guide.js +18 -18
  10. package/dist/build-static.js +136 -0
  11. package/dist/capabilities.js +85 -31
  12. package/dist/capability-query.js +0 -1
  13. package/dist/cli.js +24 -32
  14. package/dist/compliance-rules/baseline.js +2 -10
  15. package/dist/compliance-rules/privacy.js +5 -16
  16. package/dist/compliance-rules/shared.js +0 -2
  17. package/dist/compliance.js +6 -8
  18. package/dist/config.js +15 -8
  19. package/dist/context.js +9 -10
  20. package/dist/examples.js +2 -2
  21. package/dist/explain-cli.js +7 -5
  22. package/dist/explain.js +10 -5
  23. package/dist/extensions.js +61 -2
  24. package/dist/function-sources.js +34 -2
  25. package/dist/function-worker.js +3 -1
  26. package/dist/functions.js +84 -13
  27. package/dist/guest-api.js +29 -3
  28. package/dist/index.js +3 -6
  29. package/dist/manifest.js +11 -7
  30. package/dist/mcp-authoring.js +2 -2
  31. package/dist/mcp.js +1 -1
  32. package/dist/observability.js +1 -21
  33. package/dist/policies/cache.js +8 -3
  34. package/dist/policies.js +3 -1
  35. package/dist/policy.js +28 -9
  36. package/dist/prerender.js +4 -0
  37. package/dist/project-tests.js +3 -3
  38. package/dist/readiness.js +35 -9
  39. package/dist/route-diff.js +12 -5
  40. package/dist/router.js +5 -7
  41. package/dist/runtime.js +77 -58
  42. package/dist/sandbox.js +48 -0
  43. package/dist/scaffold.js +0 -0
  44. package/dist/scripts/operational-drills.js +12 -54
  45. package/dist/server.js +3 -29
  46. package/dist/tooling.js +1 -1
  47. package/dist/trusted-functions.js +210 -0
  48. package/dist/types/build-static.d.ts +43 -0
  49. package/dist/types/capabilities.d.ts +14 -5
  50. package/dist/types/compliance-rules/shared.d.ts +0 -2
  51. package/dist/types/compliance.d.ts +0 -3
  52. package/dist/types/config.d.ts +2 -1
  53. package/dist/types/context.d.ts +2 -1
  54. package/dist/types/examples.d.ts +1 -1
  55. package/dist/types/explain.d.ts +6 -0
  56. package/dist/types/extensions.d.ts +55 -0
  57. package/dist/types/function-sources.d.ts +4 -0
  58. package/dist/types/functions.d.ts +48 -5
  59. package/dist/types/guest-api.d.ts +1 -0
  60. package/dist/types/index.d.ts +3 -6
  61. package/dist/types/manifest.d.ts +5 -3
  62. package/dist/types/observability.d.ts +1 -14
  63. package/dist/types/project-tests.d.ts +1 -2
  64. package/dist/types/readiness.d.ts +12 -3
  65. package/dist/types/router.d.ts +2 -1
  66. package/dist/types/runtime.d.ts +0 -27
  67. package/dist/types/sandbox.d.ts +12 -0
  68. package/dist/types/scaffold.d.ts +0 -2
  69. package/dist/types/server.d.ts +1 -4
  70. package/dist/types/tooling.d.ts +3 -3
  71. package/dist/types/trusted-functions.d.ts +29 -0
  72. package/dist/types/types.d.ts +22 -8
  73. package/dist/types/verify-deployment.d.ts +2 -2
  74. package/dist/types.js +25 -5
  75. package/dist/typescript-authoring.js +67 -18
  76. package/dist/verify-deployment.js +3 -3
  77. package/docs/AI-AUTHORING.md +109 -10
  78. package/docs/ASSETS.md +2 -1
  79. package/docs/AUTH-BACKUP.md +32 -0
  80. package/docs/AWS.md +4 -4
  81. package/docs/BEST-PRACTICES.md +17 -8
  82. package/docs/CAPABILITIES.md +30 -17
  83. package/docs/CAPACITY.md +128 -31
  84. package/docs/CI-FOLLOWUP-2026-09-19.md +97 -0
  85. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +322 -0
  86. package/docs/CI.md +8 -3
  87. package/docs/CLOUDFLARE.md +1 -2
  88. package/docs/CODEBASE-AUDIT-2026-09-20.md +278 -0
  89. package/docs/COMPLIANCE.md +6 -9
  90. package/docs/DEPLOYMENT-CHECKS.md +1 -1
  91. package/docs/DEVELOPMENT-PIPELINE.md +181 -0
  92. package/docs/EXTENSIONS.md +193 -11
  93. package/docs/FRAMEWORK.md +53 -33
  94. package/docs/FUNCTION-SECURITY.md +173 -32
  95. package/docs/INSTALL.md +0 -5
  96. package/docs/LOAD-TESTING.md +4 -4
  97. package/docs/LOCAL-DEVELOPMENT.md +3 -0
  98. package/docs/MIDDLEWARE.md +27 -16
  99. package/docs/MONITORING.md +2 -19
  100. package/docs/OBSERVABILITY.md +7 -16
  101. package/docs/OPEN-DECISIONS.md +184 -0
  102. package/docs/OPERATIONAL-PROOF.md +26 -30
  103. package/docs/OPERATIONS.md +23 -32
  104. package/docs/POLICIES.md +27 -8
  105. package/docs/PRERENDER.md +25 -13
  106. package/docs/PROJECT-DIRECTION.md +35 -10
  107. package/docs/READINESS.md +6 -2
  108. package/docs/README.md +20 -41
  109. package/docs/RELEASE-0.4.0-alpha.3.md +50 -0
  110. package/docs/RELEASE-READINESS.md +50 -30
  111. package/docs/RELEASE-SECURITY.md +72 -86
  112. package/docs/RESILIENCE.md +16 -15
  113. package/docs/ROUTING.md +7 -10
  114. package/docs/SANDBOX-REVIEW.md +19 -6
  115. package/docs/SCAFFOLDING.md +0 -2
  116. package/docs/SECURITY-AUDIT.md +42 -2
  117. package/docs/SPECIFICATION.md +63 -27
  118. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +288 -0
  119. package/docs/SPIKE-BUSINESS-SUITE.md +16 -0
  120. package/docs/SPIKE-CORE-LAYERING.md +368 -0
  121. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +211 -0
  122. package/docs/SPIKE-LAMBDA-COMPILE.md +12 -2
  123. package/docs/STARTERS.md +1 -1
  124. package/docs/STATIC.md +105 -0
  125. package/docs/TOOLING.md +17 -12
  126. package/docs/TUNNELS.md +0 -3
  127. package/docs/TYPESCRIPT-AUTHORING.md +32 -12
  128. package/docs/TYPESCRIPT.md +25 -4
  129. package/docs/VERCEL.md +4 -5
  130. package/docs/VERSION-ALIGNMENT.md +46 -0
  131. package/docs/YAML-GUIDE.md +0 -3
  132. package/docs/YAML-REFERENCE.md +16 -6
  133. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +68 -0
  134. package/docs/{MANAGEMENT-SECURITY.md → archive/2026-09-19/MANAGEMENT-SECURITY.md} +20 -0
  135. package/docs/{NEXT-PHASE-PLAN.md → archive/2026-09-19/NEXT-PHASE-PLAN.md} +26 -8
  136. package/docs/{NEXT-STEPS.md → archive/2026-09-19/NEXT-STEPS.md} +274 -43
  137. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +277 -0
  138. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +186 -0
  139. package/docs/archive/2026-09-19/ROADMAP.md +386 -0
  140. package/docs/{SPIKE-EXTENSION-MODEL.md → archive/2026-09-19/SPIKE-EXTENSION-MODEL.md} +12 -2
  141. package/docs/{SPIKE-EXTENSIONS.md → archive/2026-09-19/SPIKE-EXTENSIONS.md} +28 -12
  142. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +776 -0
  143. package/docs/{USABILITY-REVIEW.md → archive/2026-09-19/USABILITY-REVIEW.md} +18 -2
  144. package/docs/archive/README.md +27 -0
  145. package/docs/policies/agents.md +1 -1
  146. package/docs/policies/cache.md +13 -0
  147. package/docs/policies/compression.md +3 -2
  148. package/docs/policies/security.md +3 -2
  149. package/examples/assets/example.yaml +2 -2
  150. package/examples/aws/example.yaml +1 -1
  151. package/examples/cloudflare/example.yaml +1 -1
  152. package/examples/conditions/example.yaml +1 -1
  153. package/examples/cookbook/example.yaml +1 -1
  154. package/examples/cookbook/middleware/bucket.mjs +12 -2
  155. package/examples/cookbook/middleware/locale.mjs +7 -3
  156. package/examples/cookbook/route-index.json +1 -1
  157. package/examples/cookbook/routes/middleware.yaml +1 -1
  158. package/examples/egress/example.yaml +1 -1
  159. package/examples/extensions/example.yaml +1 -1
  160. package/examples/prerender/README.md +14 -6
  161. package/examples/prerender/example.yaml +1 -1
  162. package/examples/prerender/functions/page.mjs +4 -2
  163. package/examples/prerender/middleware/template.mjs +1 -1
  164. package/examples/prerender/prerender.mjs +1 -1
  165. package/examples/prerender/urlcode.yaml +8 -4
  166. package/examples/provider-conformance/example.yaml +1 -1
  167. package/examples/vercel/example.yaml +1 -1
  168. package/llms-full.txt +686 -239
  169. package/llms.txt +27 -15
  170. package/package.json +32 -5
  171. package/packaging/claude-plugin/.claude-plugin/plugin.json +2 -2
  172. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +36 -22
  173. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +16 -22
  174. package/recipes/authenticated-json-api/README.md +4 -3
  175. package/recipes/authenticated-json-api/functions/profile.mjs +2 -1
  176. package/recipes/authenticated-json-api/recipe.yaml +2 -2
  177. package/recipes/contact-form/functions/contact.mjs +2 -1
  178. package/recipes/contact-form/recipe.yaml +3 -3
  179. package/recipes/cors-api/README.md +2 -2
  180. package/recipes/cors-api/recipe.yaml +2 -2
  181. package/recipes/health-page/README.md +1 -1
  182. package/recipes/health-page/recipe.yaml +1 -1
  183. package/recipes/json-api/README.md +1 -1
  184. package/recipes/json-api/recipe.yaml +4 -4
  185. package/recipes/middleware/README.md +8 -4
  186. package/recipes/middleware/middleware/bucket.mjs +12 -2
  187. package/recipes/middleware/middleware/locale.mjs +7 -3
  188. package/recipes/middleware/recipe.yaml +1 -1
  189. package/recipes/protected-download/README.md +1 -1
  190. package/recipes/protected-download/recipe.yaml +2 -2
  191. package/recipes/redirect/recipe.yaml +1 -1
  192. package/recipes/static-plus-api/README.md +2 -2
  193. package/recipes/static-plus-api/public/index.html +1 -1
  194. package/recipes/static-plus-api/recipe.yaml +2 -2
  195. package/recipes/static-plus-api/urlcode.yaml +1 -1
  196. package/recipes/typescript/README.md +2 -1
  197. package/recipes/typescript/recipe.yaml +5 -5
  198. package/recipes/webhook-receiver/README.md +5 -1
  199. package/recipes/webhook-receiver/recipe.yaml +2 -1
  200. package/recipes/webhook-receiver/urlcode.yaml +7 -0
  201. package/schemas/recipe.schema.json +4 -3
  202. package/schemas/urlcode.schema.json +14 -41
  203. package/skills/urlcode/SKILL.md +32 -11
  204. package/starters/default/AGENTS.md +18 -18
  205. package/starters/default/urlcode.yaml +0 -1
  206. package/dist/link-api.js +0 -136
  207. package/dist/link-cli.js +0 -141
  208. package/dist/link-events.js +0 -76
  209. package/dist/link-records.js +0 -31
  210. package/dist/link-store-worker.js +0 -150
  211. package/dist/link-store.js +0 -250
  212. package/dist/management-policy.js +0 -40
  213. package/dist/sqlite-version.js +0 -6
  214. package/dist/types/link-api.d.ts +0 -30
  215. package/dist/types/link-cli.d.ts +0 -37
  216. package/dist/types/link-events.d.ts +0 -27
  217. package/dist/types/link-records.d.ts +0 -11
  218. package/dist/types/link-store-worker.d.ts +0 -1
  219. package/dist/types/link-store.d.ts +0 -130
  220. package/dist/types/management-policy.d.ts +0 -8
  221. package/dist/types/sqlite-version.d.ts +0 -1
  222. package/docs/DYNAMIC-LINKS.md +0 -61
  223. package/docs/links/cli.md +0 -110
  224. package/docs/links/limits.md +0 -175
  225. package/docs/links/management-api.md +0 -80
  226. package/docs/links/pools.md +0 -75
  227. package/docs/links/setup.md +0 -135
  228. package/docs/yaml/links.md +0 -30
  229. package/examples/live-links/README.md +0 -11
  230. package/examples/live-links/example.yaml +0 -21
  231. package/examples/live-links/tests/requests.json +0 -6
  232. package/examples/live-links/urlcode.yaml +0 -16
@@ -1,10 +1,17 @@
1
1
  # Operator-installed extensions
2
2
 
3
- Extensions are trusted operator modules, separate from application WASM. Auth
3
+ Extensions are trusted operator modules, separate from a project's own
4
+ `function`/`middleware` code. Auth
4
5
  and admin implementations live in `urlcode-auth` and `urlcode-admin`; the runtime
5
6
  supplies only the generic integration contract. No project file can import a host
6
7
  extension or choose its npm package.
7
8
 
9
+ Stored short links moved out of core this way too: a `urlcode-dynamic-link`
10
+ package (mount-based, like `auth`/`admin`) owned the durable link store, its CLI
11
+ and management API. That package has since been retired and unpublished, so no
12
+ supported stored-link extension ships today. Core no longer has a native `link`
13
+ handler or a `dynamicLinks` project flag.
14
+
8
15
  A project declares versioned configuration and exclusive route mounts:
9
16
 
10
17
  ```yaml
@@ -52,9 +59,9 @@ fails, naming the route, when `auth` appears without an `extensions.auth`
52
59
  declaration, next to `policies.extensions.auth`, or next to
53
60
  `policies.extensions: false`.
54
61
 
55
- The same shape is reserved for the cache policy: a future `cache: {strategy,
56
- maxAge}` route key may expand to `policies.cache` in the same pass. It is not
57
- implemented; declare `policies.cache` today (see [policies](POLICIES.md)).
62
+ The same shape is used for the cache policy: a route-level `cache: {strategy,
63
+ maxAge, ...}` expands to `policies.cache` in the same pass (see
64
+ [policies](POLICIES.md)).
58
65
 
59
66
  The configuration and requirement objects above are validated by the installed
60
67
  extension's schemas. They are examples of extension-owned fields, not built-in
@@ -71,17 +78,86 @@ project and place the exact returned SHA-256 in each registration's
71
78
  in the revision. Changing them requires an explicit operator reapproval.
72
79
 
73
80
  Registrations provide a name, contract version, target list, JSON configuration
74
- schema, optional policy schema and activation factory. Activation receives the
81
+ schema, optional policy schema, an optional declared `cacheSensitive` (below)
82
+ and activation factory. Activation receives the
75
83
  canonical operator origin, target, revision and mount bases. Its instance handles
76
- bounded requests and, when used in policies, authorizes requests. Missing
84
+ bounded requests and, when named in a route's policies, gates the request via
85
+ `authorize`, wraps the rest of the pipeline via `middleware`, or both (see
86
+ [Wrapping a route](#wrapping-a-route-extension-middleware) above). Missing
77
87
  registrations, stale grants, invalid configuration and unsupported targets fail
78
88
  activation. Multiple mounts cannot overlap other declared routes.
79
89
 
80
90
  For extension-protected routes, agents/throttle run before authorization and
81
- cache access happens only after authorization. Extension routes and protected
82
- routes reject cache strategies other than no-store; every resulting response is
83
- forced to no-store after host response hooks. Compression is disabled on these
84
- responses.
91
+ cache access happens only after authorization. This part is unconditional:
92
+ naming any extension in `policies.extensions` always runs its `authorize()`
93
+ (when it implements one) before the route's own handler, whatever this
94
+ section says next.
95
+
96
+ An `extension:` mount is always confidential: its route rejects cache
97
+ strategies other than no-store, and every response is forced to no-store
98
+ after host response hooks, with compression disabled. A `policies.extensions`
99
+ route (no mount, `authorize`/`middleware` only) gets the same treatment
100
+ **unless every extension it names explicitly declares
101
+ `cacheSensitive: false`** on its `RuntimeExtension` registration. That field
102
+ defaults to sensitive (unset or `true`): the safe default is unchanged, and
103
+ relaxing it is an explicit, reviewed operator opt-in an extension author
104
+ makes once, in host code, never inferred from a route or from a response the
105
+ extension happens to return. It exists for a generic, cache-transparent
106
+ extension whose `middleware()` is pure request/response wrapping with no
107
+ access-control semantics of its own (a logging or header-rewriting
108
+ extension, for example) — declared this way, its wrapped route keeps
109
+ whatever `Cache-Control` its own handler sets, exactly like the native
110
+ `middleware:` array already does, and compression is not disabled either. A
111
+ route naming more than one extension stays confidential if any one of them
112
+ is sensitive (or leaves the field unset); one `cacheSensitive: false`
113
+ extension cannot relax a route that also names a sensitive one. This can
114
+ only relax the no-store floor a generic extension would otherwise inherit —
115
+ it has no effect on `authorize()`, which runs the same way regardless, and
116
+ `auth`/`admin`-style extensions gating real access must leave it at the
117
+ default.
118
+
119
+ ## Wrapping a route: extension middleware
120
+
121
+ `authorize` is a gate: it runs once, before the route's handler, and can only
122
+ either let the request through unchanged or answer instead of it. It cannot
123
+ see or change what the handler itself returns.
124
+
125
+ `middleware` is a wrap. An extension instance may implement it alongside or
126
+ instead of `authorize`, attached the same way, via
127
+ `policies.extensions.<name>` on a route (no `extension:` mount required); its
128
+ `config` is exactly the same per-route value `authorize`'s `requirement`
129
+ receives, validated once against the extension's `policySchema`:
130
+
131
+ ```ts
132
+ middleware?(config: Readonly<Record<string, unknown>>, request: ExtensionRequest,
133
+ next: () => Promise<HandlerResult>): HandlerResult | Promise<HandlerResult>;
134
+ ```
135
+
136
+ `next()` invokes the rest of the pipeline for that route: any other extension
137
+ `middleware()` also declared on the route (see below), then the route's own
138
+ native `middleware:` chain and handler, dispatched through the sandboxed or
139
+ trusted engine exactly as it is today. Calling it lets the hook run code
140
+ before and after the rest of the pipeline, inspecting or mutating the
141
+ `HandlerResult` it resolves to — the same "add a header to whatever the
142
+ handler returns" shape as the native `middleware/headers.mjs` cookbook
143
+ recipe, but declared by an operator-installed extension instead of project
144
+ code. Skipping it short-circuits everything after that point, the same
145
+ capability `authorize` already has, just usable from either side of the
146
+ handler now. `next()` may be called at most once; calling it again throws.
147
+
148
+ A route naming more than one extension in `policies.extensions` chains every
149
+ one that implements `middleware`, in the order the keys are declared, each
150
+ one's `next()` reaching the next one and the innermost `next()` reaching the
151
+ native pipeline — the first declared name is outermost. This is purely
152
+ additive at the `policies.extensions` layer and never touches the native
153
+ `middleware:` array, its schema, or its dispatch, all of which are unchanged.
154
+
155
+ `authorize` and `middleware` compose on the same route, from the same or
156
+ different extensions, without special-casing: `authorize` always runs first
157
+ (unchanged), and any declared `middleware()` wraps everything after that
158
+ point, including the rest of the authorize-gated pipeline. A route naming an
159
+ extension via `policies.extensions` only requires that extension to
160
+ implement `authorize`, `middleware`, or both — never both unconditionally.
85
161
 
86
162
  One exception exists for content-hashed assets. A registration may declare
87
163
  `immutableAssets: {prefix: '/static'}`, a normalized literal path under each
@@ -105,6 +181,112 @@ HTML/JS on an authentication origin must be trusted by that site's operator.
105
181
  Cloudflare refuses extensions until its artifact format supports their execution.
106
182
  Node adapter conformance is not a live-provider deployment claim.
107
183
 
184
+ ## Project-level lifecycle hooks
185
+
186
+ `authorize` and `middleware` let core call *into* an extension. They do not
187
+ let a project hand its own code *to* an extension to run at a defined point.
188
+ That gap matters once an extension has meaningful lifecycle events —
189
+ registration, deletion, an administrative action, a link resolution, and so
190
+ on. Presentation already has a standard layering mechanism for this: a
191
+ project customizes an extension's *look* through `urlcode-ui`'s
192
+ `copy`/`extra.css`/`templates` without forking it. Behavior needs the same
193
+ standard, or every extension author (and every project depending on one)
194
+ either reinvents it or forks the extension. `urlcode-auth` and
195
+ `urlcode-admin` independently hit this gap (auth/#35, admin/#32); auth's own
196
+ [SPIKE-AUTH.md](https://github.com/jimhoyd-com/urlcode-auth/blob/main/docs/SPIKE-AUTH.md)
197
+ already scoped a shape for `onSignUp`/`beforeRegister`/`onDelete`.
198
+
199
+ **The pattern.** An extension with lifecycle events an author judges worth
200
+ exposing should let the project name its own function in the extension's own
201
+ `config`, using the same source shape `function`/`middleware` routes already
202
+ use (a string path, or `{source, export, args}` — `schemas/urlcode.schema.json`),
203
+ and add its own `sandbox` boolean next to it (below). The extension's own
204
+ `activate()` reads that config, and its own runtime dispatch — not a new core
205
+ primitive, an ordinary call the extension package makes with the request
206
+ context it already has — invokes the named function at the lifecycle point
207
+ it defines, with a typed input and a typed verdict the extension's own
208
+ schema documents. For example, an auth-style extension might declare:
209
+
210
+ ```yaml
211
+ extensions:
212
+ auth:
213
+ version: "1"
214
+ config:
215
+ hooks:
216
+ beforeRegister:
217
+ source: ./hooks/registration-rule.mjs
218
+ export: default
219
+ onSignUp:
220
+ source: ./hooks/on-signup.mjs
221
+ ```
222
+
223
+ with `beforeRegister` called before an account is created, given a typed
224
+ `{email, traits}`-shaped input and returning a typed verdict (`{allow: true}`
225
+ or `{allow: false, reason}`), and `onSignUp` called after, for side effects
226
+ such as provisioning a workspace. The exact hook names, input/verdict shapes
227
+ and invocation points are the extension's own design — `hooks` is not a core
228
+ schema key — but the source/export/sandbox shape, and the fact that this is
229
+ project code the extension calls rather than an operator callback in
230
+ `host.mjs`, should be consistent across extensions so an author who has
231
+ learned one has learned the pattern.
232
+
233
+ **Trust and execution mode.** Project-level lifecycle hooks are first-party
234
+ project code, the same category as any `function`/`middleware` route, and
235
+ follow the same trusted-by-default rule with no special case
236
+ (docs/SPIKE-DEFAULT-TRUST-MODEL.md, [FUNCTION-SECURITY.md](FUNCTION-SECURITY.md)):
237
+ trusted, in-process execution by default; a project sets `sandbox: true` on
238
+ a given hook to opt that hook into isolated QuickJS/WASM execution, exactly
239
+ the mechanism `function`/`middleware` routes already use and for the same
240
+ reason — the hook's own code, not the trustworthiness of whatever triggered
241
+ it, is what calls for isolation (docs/AI-AUTHORING.md's "Deciding when a
242
+ route needs `sandbox: true`"). This was raised as an open question — whether
243
+ a lifecycle hook should always run sandboxed because it makes a
244
+ security-relevant decision — and settled the other way: uniformity with the
245
+ rest of the trust model was chosen over hardwiring isolation for lifecycle
246
+ hooks specifically, the same "no special case" call already made between
247
+ `function` and `middleware` trust. A `beforeRegister` hook enforcing "only
248
+ `@acme.com` may register" is the project's own governance rule over its own
249
+ signup flow; it is not more dangerous than any other route the project
250
+ wrote, and does not get a different default.
251
+
252
+ Core's own trusted/sandboxed dispatch (`TrustedFunctions`/`FunctionPool`,
253
+ `src/runtime.ts`) is wired to route dispatch, not exposed to extensions — but
254
+ each half of a hook's `sandbox: true` opt-in has its own answer:
255
+
256
+ - **Trusted (the default, no `sandbox: true`).** No core primitive is needed
257
+ or provided: this is ordinary first-party project code, and the
258
+ extension's own `activate()` already has `ExtensionActivation.root` to
259
+ resolve the hook's `source` against and can `import()` it directly, the
260
+ same way any trusted `function`/`middleware` route does. Do that import
261
+ with a per-activation cache-busting query, the way core's own trusted
262
+ route activation does (`src/trusted-functions.ts`): Node's ESM loader
263
+ caches a resolved module forever by URL, so a plain `import()` of the
264
+ unchanged file URL makes a second activation in the same process keep
265
+ serving the hook code that was on disk at the first one
266
+ (jimhoyd-com/urlcode#198). Only the hook's **entry** module is refreshed
267
+ this way — modules the hook itself imports stay on Node's module cache,
268
+ the same limitation the trusted route path has, so a change to a hook's
269
+ own dependency still needs a process restart.
270
+ - **Sandboxed (`sandbox: true`).** `@jimhoyd/urlcode/sandbox` exports
271
+ `SandboxPool`, the same QuickJS/worker-thread engine that backs a
272
+ sandboxed `function`/`middleware` route — the identical module-allowlist
273
+ walk, memory/stack limits, two-layer deadline enforcement, `maxBytes` and
274
+ response-shape validation, with no separate or weaker engine for
275
+ extensions. It takes an explicit list of `{source, export}` entries
276
+ (resolve a hook's `source` string with the re-exported `functionFile()`,
277
+ the same resolution/validation a native route's `source` gets) instead of
278
+ anything route/YAML-shaped, and `execute({entry, chain}, request, context)`
279
+ in place of a `FunctionRoute`. There is no "trusted" mode exported
280
+ alongside it — `SandboxPool` is only ever the isolated path; see
281
+ [FUNCTION-SECURITY.md](FUNCTION-SECURITY.md) and
282
+ [TYPESCRIPT.md](TYPESCRIPT.md) for the full contract.
283
+
284
+ An extension honoring a hook's `sandbox: true` is expected to actually
285
+ isolate that invocation through `SandboxPool` now that the primitive exists
286
+ (or document plainly that it does not yet, rather than accepting the field
287
+ and silently running it trusted) — say which, in the extension's own docs,
288
+ so an author reading them is not misled about what opt-in exists.
289
+
108
290
  ## Discovering schemas
109
291
 
110
292
  Each registration carries the JSON Schemas that validate its `config` block and
@@ -161,7 +343,7 @@ and each extension's own operator files. Core never bundles or imports the
161
343
  extension packages at build time; at run time it resolves
162
344
  `@jimhoyd/urlcode-<name>` for each name with Node's package resolution from
163
345
  the invoking directory (so `npm install @jimhoyd/urlcode-auth` in that
164
- directory, from npm where the packages are published as `0.1.0-alpha.1`
346
+ directory, from npm where the packages are published as `0.1.0-alpha.x`
165
347
  prereleases, is the normal path and what makes `--with auth` work), imports
166
348
  the package and calls its
167
349
  `scaffold` export with this request:
package/docs/FRAMEWORK.md CHANGED
@@ -9,14 +9,14 @@ claim here is implemented in the linked repository; nothing is roadmap.
9
9
 
10
10
  | Package | Repository | What it adds | How a project declares it |
11
11
  |---|---|---|---|
12
- | `@jimhoyd/urlcode` | [urlcode](https://github.com/jimhoyd-com/urlcode) | The runtime: YAML routes, isolated functions and middleware, pages and assets, live short links, policies, site conventions, CLI, provider adapters, the extension contract | `urlcode.yaml` with `version: "1"` |
12
+ | `@jimhoyd/urlcode` | [urlcode](https://github.com/jimhoyd-com/urlcode) | The runtime: YAML routes, functions and middleware (trusted by default, `sandbox: true` opt-in), pages and assets, policies, site conventions, CLI, provider adapters, the extension contract | `urlcode.yaml` with `version: "1"` |
13
13
  | `@jimhoyd/urlcode-ui` | [urlcode-ui](https://github.com/jimhoyd-com/urlcode-ui) | Shared presentation: escaped templates, shadcn/ui partials, one stylesheet with light and dark, themes, translations, the `ui` extension that serves the kit's assets | `extensions.ui` plus an asset mount route |
14
14
  | `@jimhoyd/urlcode-auth` | [urlcode-auth](https://github.com/jimhoyd-com/urlcode-auth) | Accounts: password, passkeys, OpenID Connect, email codes, TOTP, recovery, sessions, roles, registration modes, account page, operator CLI | `extensions.auth` plus an `/account/*` mount and `policies.extensions.auth` on protected routes |
15
15
  | `@jimhoyd/urlcode-admin` | [urlcode-admin](https://github.com/jimhoyd-com/urlcode-admin) | Administration: users, sessions, roles, audit, registration approval, two-person cases, support impersonation, health | `extensions.admin` plus an `/admin/*` mount |
16
16
 
17
17
  The core is Apache-2.0 and released. The three extension packages are
18
- Apache-2.0 and published to npm as alphas (`@jimhoyd/urlcode-ui@0.1.0-alpha.1`,
19
- `@jimhoyd/urlcode-auth@0.1.0-alpha.1`, `@jimhoyd/urlcode-admin@0.1.0-alpha.1`,
18
+ Apache-2.0 and published to npm as alphas (`@jimhoyd/urlcode-ui@0.1.0-alpha.4`,
19
+ `@jimhoyd/urlcode-auth@0.1.0-alpha.2`, `@jimhoyd/urlcode-admin@0.1.0-alpha.2`,
20
20
  on core `0.4.0-alpha.1`). An alpha on npm is a distribution channel, not an
21
21
  endorsement: the source is complete, but independent review, deployment
22
22
  evidence and an accessibility assessment are still pending
@@ -24,6 +24,9 @@ evidence and an accessibility assessment are still pending
24
24
  files say exactly what is built: [auth](https://github.com/jimhoyd-com/urlcode-auth/blob/main/IMPLEMENTATION-STATUS.md),
25
25
  [admin](https://github.com/jimhoyd-com/urlcode-admin/blob/main/IMPLEMENTATION-STATUS.md),
26
26
  [ui](https://github.com/jimhoyd-com/urlcode-ui/blob/main/IMPLEMENTATION-STATUS.md).
27
+ Which core version each package supports, how it declares that, and the order
28
+ in which a core change reaches the downstream repositories are recorded in
29
+ [core version alignment](VERSION-ALIGNMENT.md).
27
30
 
28
31
  ## The ladder
29
32
 
@@ -38,22 +41,27 @@ Each rung's YAML is valid on every rung above it.
38
41
  llms.txt) and `policies` (throttle, agents, security headers, compression,
39
42
  cache). Still no code.
40
43
  3. **Functions and middleware.** `function` routes and ordered `middleware`
41
- in isolated JavaScript (QuickJS inside WebAssembly, fresh heap per call, no
42
- Node, filesystem or network). Secrets reach a function only through an
43
- operator grant pinned to the project revision.
44
- 4. **Live short links.** A `link` route backed by an optional SQLite store,
45
- with CLI and a private management API. Records change without reloads.
46
- 5. **Accounts.** The `auth` extension: sign-in, registration, MFA, account
44
+ in JavaScript, trusted and in-process by default; a route declaring
45
+ `sandbox: true` runs isolated instead (QuickJS inside WebAssembly, fresh
46
+ heap per call, no Node, filesystem or network). Secrets reach a function
47
+ only through an operator grant pinned to the project revision.
48
+ 4. **Accounts.** The `auth` extension: sign-in, registration, MFA, account
47
49
  page and protected routes. The operator installs it in a host file outside
48
50
  the project; YAML only declares the mount and configuration.
49
- 6. **Administration.** The `admin` extension on the same service: manage the
51
+ 5. **Administration.** The `admin` extension on the same service: manage the
50
52
  people who signed up, their sessions and roles, review the audit trail.
51
- 7. **Your own look.** A shared `presentation` (catalogue and theme variables)
53
+
54
+ 6. **Your own look.** A shared `presentation` (catalogue and theme variables)
52
55
  restyles auth and admin together; the `ui` extension adds the template kit,
53
56
  project copy, template and stylesheet overrides for kit-rendered pages.
54
57
 
55
- Rungs 1 to 4 need only the core package. Rungs 5 to 7 need the extension
56
- packages, installed from npm as `0.1.0-alpha.1` prereleases, and a Node host
58
+ Stored short links previously sat here as a native `link` route; that handler
59
+ was removed from core. A `urlcode-dynamic-link` package owned them the same way
60
+ `auth`/`admin` own their mounts, but it has been retired and unpublished; no
61
+ package occupies this rung today.
62
+
63
+ Rungs 1 to 3 need only the core package. Rungs 4 to 6 need the extension
64
+ packages, installed from npm as `0.1.0-alpha.x` prereleases, and a Node host
57
65
  with a patched SQLite build; see each repository's README for the exact
58
66
  requirement.
59
67
 
@@ -66,9 +74,9 @@ npm install @jimhoyd/urlcode @jimhoyd/urlcode-ui @jimhoyd/urlcode-auth @jimhoyd/
66
74
  urlcode init my-site --with auth,admin
67
75
  ```
68
76
 
69
- Installing from npm is the normal path; the extension repositories'
70
- `pack-sources.mjs` still builds local tarballs from a reviewed checkout for
71
- operators who install only source they have read. Three files make an extended
77
+ Installing from npm is the normal path; `scripts/pack-sources.mjs` still builds
78
+ local tarballs from a reviewed checkout for operators who install only source
79
+ they have read one revision now covers core and every extension. Three files make an extended
72
80
  project. Nothing else is discovered by convention.
73
81
 
74
82
  ```
@@ -100,7 +108,8 @@ routes:
100
108
  auth: {}
101
109
  ```
102
110
 
103
- The operator host wires the packages, once, in code the project cannot reach:
111
+ The operator host explicitly registers the packages. Registration is an
112
+ activation boundary; it does not isolate trusted application code from the host:
104
113
 
105
114
  ```js
106
115
  import { createUiExtension } from '@jimhoyd/urlcode-ui/host';
@@ -120,12 +129,12 @@ export default {
120
129
  };
121
130
  ```
122
131
 
123
- Auth and admin pages currently render through the shared primitives and a
124
- `presentation` (catalogue and `--ui-*` theme variables). The `ui` extension
125
- serves the template kit and its assets for pages an extension renders through
126
- `ui.kit`; moving the auth and admin screens onto the kit is a listed remaining
127
- item in the ui repository, so today one `presentation` restyles them and the
128
- `ui` block is optional.
132
+ Auth and admin already render their screens through `ui.kit` when the host
133
+ supplies the UI extension; their package-owned templates and catalogues must be
134
+ registered with that kit. Without it, both retain shared primitive rendering
135
+ through `presentation`. The example above uses that primitive fallback. The UI
136
+ block is optional, and a migration to the kit is not unfinished framework work.
137
+ See each package README for its complete kit registration example.
129
138
 
130
139
  ```sh
131
140
  urlcode serve --project /absolute/site --host-file /absolute/operator/host.mjs --origin https://site.example
@@ -147,26 +156,37 @@ or mounts changes the revision and needs an explicit operator reapproval.
147
156
  These are the facts that keep generated projects valid. The full matrix is in
148
157
  [AI authoring](AI-AUTHORING.md); this is the short list.
149
158
 
150
- - **YAML never names code, packages, databases or credentials.** Extensions are
159
+ - **Extension YAML names logical extensions, not host packages or credentials.**
160
+ Function and middleware `source` fields do name project modules. Extensions are
151
161
  logical names; the host file chooses the implementation. There is no
152
162
  `--extension` flag, no `import` in YAML, no interpolation.
153
163
  - **One handler per route.** `redirect`, `respond`, `page`, `static`, `download`,
154
- `function`, `link`, `proxy`, `conditional` or `extension`, plus optional
164
+ `function`, `proxy`, `conditional` or `extension`, plus optional
155
165
  `middleware`. Paths are exact or single-segment `{param}`; `/*` only on
156
166
  `static` and `extension` mounts. No regex.
157
- - **Guest code is sandboxed.** Functions see a text/JSON `Request`/`Response`
158
- subset, validated `args` and granted `env`. No `fetch`, Node, filesystem or
159
- timers. Do not write code that needs them; declare a `proxy` or a binding.
167
+ - **`function`/`middleware` code is trusted by default, sandboxed opt-in.**
168
+ It runs in-process with full Node access unless the route declares
169
+ `sandbox: true`, which isolates it to a text/JSON `Request`/`Response`
170
+ subset, validated `args` and granted `env`, with no `fetch`, Node,
171
+ filesystem or general network access; bounded timers are available. Either way, `args`/`env`/`secrets` are exactly what
172
+ the route declares and an operator grants — trust changes where code runs,
173
+ not what it is handed. See docs/SPIKE-DEFAULT-TRUST-MODEL.md and
174
+ docs/FUNCTION-SECURITY.md.
160
175
  - **Authentication is host processing.** Do not build login forms, session
161
- cookies or password checks in functions. Declare `policies.extensions.auth`
162
- on the route; the runtime withholds `Cookie` and `Authorization` from guests.
176
+ cookies or password checks in functions. With the auth extension declared,
177
+ prefer `auth: true` or `auth: {role: admin}`; these expand to
178
+ `policies.extensions.auth`. The runtime filters credential headers passed to
179
+ application handlers. This is not a security boundary against trusted Node code.
163
180
  - **Everything is validated before it runs.** `urlcode validate --local`,
164
181
  `urlcode test`, `urlcode audit --expect-routes N`. Unsupported features fail
165
182
  with the route named; nothing degrades silently.
166
183
  - **Provider targets refuse what they cannot enforce.** Cloudflare runs
167
184
  redirects and declared responses only. Serverless adapters refuse functions,
168
- links, proxy, signals and extensions. Check
169
- `urlcode capabilities --target NAME` before promising a deployment.
185
+ proxy, signals and extensions. The `static` target (S3 + CloudFront,
186
+ no server) refuses everything that needs request-time logic, keeping only
187
+ `redirect`/`respond`/`page`/`static`/`download` — see [static
188
+ hosting](STATIC.md). Check `urlcode capabilities --target NAME` before
189
+ promising a deployment.
170
190
  - **Report evidence, not hope.** The commands above are the evidence. Local
171
191
  tests are not deployment, soak or independent security review.
172
192
 
@@ -178,5 +198,5 @@ These are the facts that keep generated projects valid. The full matrix is in
178
198
  | Add accounts | [auth README](https://github.com/jimhoyd-com/urlcode-auth#readme), [auth security](https://github.com/jimhoyd-com/urlcode-auth/blob/main/SECURITY.md) |
179
199
  | Add administration | [admin README](https://github.com/jimhoyd-com/urlcode-admin#readme) |
180
200
  | Restyle every page | [ui README](https://github.com/jimhoyd-com/urlcode-ui#readme), [ui contract](https://github.com/jimhoyd-com/urlcode-ui/blob/main/CONTRACT.md) |
181
- | Write an extension | [extensions](EXTENSIONS.md), [extension model review](SPIKE-EXTENSION-MODEL.md) |
201
+ | Write an extension | [extensions](EXTENSIONS.md), [extension model review](archive/2026-09-19/SPIKE-EXTENSION-MODEL.md) |
182
202
  | Run it | [operations](OPERATIONS.md), [install](INSTALL.md), [deployment checks](DEPLOYMENT-CHECKS.md) |