@jimhoyd/urlcode 0.4.1 → 0.4.6

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 (212) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +60 -7
  2. package/.claude/skills/urlcode-operations/SKILL.md +4 -0
  3. package/README.md +19 -15
  4. package/SECURITY.md +5 -3
  5. package/dist/BUILD-MANIFEST.json +31 -28
  6. package/dist/agent-context.js +82 -0
  7. package/dist/agents-guide.js +42 -42
  8. package/dist/authoring.js +12 -2
  9. package/dist/body-schema.js +159 -0
  10. package/dist/build-cloudflare.js +2 -0
  11. package/dist/capabilities.js +1 -1
  12. package/dist/cli.js +29 -12
  13. package/dist/config.js +74 -6
  14. package/dist/context.js +4 -6
  15. package/dist/errors.js +3 -1
  16. package/dist/examples.js +1 -1
  17. package/dist/extensions.js +138 -3
  18. package/dist/http-policy.js +19 -4
  19. package/dist/http-response.js +2 -2
  20. package/dist/init-with.js +71 -9
  21. package/dist/mcp.js +15 -2
  22. package/dist/pattern-guard.js +32 -0
  23. package/dist/policies/security.js +0 -0
  24. package/dist/policy.js +16 -0
  25. package/dist/project-tests.js +35 -11
  26. package/dist/readiness.js +188 -32
  27. package/dist/recipes.js +1 -1
  28. package/dist/router.js +17 -0
  29. package/dist/runtime.js +26 -3
  30. package/dist/scaffold.js +0 -0
  31. package/dist/server.js +26 -3
  32. package/dist/site.js +0 -0
  33. package/dist/tooling.js +3 -1
  34. package/dist/types/agent-context.d.ts +44 -0
  35. package/dist/types/authoring.d.ts +3 -1
  36. package/dist/types/body-schema.d.ts +58 -0
  37. package/dist/types/config.d.ts +10 -2
  38. package/dist/types/context.d.ts +1 -1
  39. package/dist/types/errors.d.ts +9 -1
  40. package/dist/types/examples.d.ts +1 -1
  41. package/dist/types/extensions.d.ts +105 -2
  42. package/dist/types/http-policy.d.ts +3 -0
  43. package/dist/types/init-with.d.ts +10 -1
  44. package/dist/types/pattern-guard.d.ts +10 -0
  45. package/dist/types/project-tests.d.ts +9 -0
  46. package/dist/types/readiness.d.ts +71 -0
  47. package/dist/types/recipes.d.ts +1 -1
  48. package/dist/types/runtime.d.ts +4 -0
  49. package/dist/types/server.d.ts +9 -1
  50. package/dist/types/tooling.d.ts +2 -0
  51. package/dist/types/types.d.ts +13 -0
  52. package/dist/types.js +10 -2
  53. package/dist/typescript-authoring.js +5 -3
  54. package/dist/verify-deployment.js +16 -10
  55. package/examples/body-validation/README.md +16 -0
  56. package/examples/body-validation/example.yaml +17 -0
  57. package/examples/body-validation/tests/requests.json +15 -0
  58. package/examples/body-validation/urlcode.yaml +34 -0
  59. package/examples/coverage-waiver/README.md +8 -0
  60. package/examples/coverage-waiver/example.yaml +16 -0
  61. package/examples/coverage-waiver/functions/notes.mjs +2 -0
  62. package/examples/coverage-waiver/tests/requests.json +3 -0
  63. package/examples/coverage-waiver/urlcode.yaml +10 -0
  64. package/examples/data-dir/README.md +39 -0
  65. package/examples/data-dir/data/welcome.txt +1 -0
  66. package/examples/data-dir/example.yaml +22 -0
  67. package/examples/data-dir/functions/note.mjs +18 -0
  68. package/examples/data-dir/tests/requests.json +6 -0
  69. package/examples/data-dir/urlcode.yaml +6 -0
  70. package/examples/lifecycle/README.md +15 -0
  71. package/examples/lifecycle/example.yaml +19 -0
  72. package/examples/lifecycle/functions/notes.mjs +29 -0
  73. package/examples/lifecycle/tests/requests.json +12 -0
  74. package/examples/lifecycle/urlcode.yaml +29 -0
  75. package/examples/not-found/README.md +10 -0
  76. package/examples/not-found/example.yaml +17 -0
  77. package/examples/not-found/public/404.html +3 -0
  78. package/examples/not-found/public/index.html +3 -0
  79. package/examples/not-found/tests/requests.json +7 -0
  80. package/examples/not-found/urlcode.yaml +7 -0
  81. package/examples/shared-blocks/README.md +11 -0
  82. package/examples/shared-blocks/example.yaml +17 -0
  83. package/examples/shared-blocks/tests/requests.json +7 -0
  84. package/examples/shared-blocks/urlcode.yaml +37 -0
  85. package/llms-full.txt +436 -149
  86. package/llms.txt +44 -6
  87. package/package.json +15 -22
  88. package/recipes/static-page/README.md +9 -0
  89. package/recipes/static-page/public/index.html +11 -0
  90. package/recipes/static-page/recipe.yaml +21 -0
  91. package/recipes/static-page/tests/requests.json +22 -0
  92. package/recipes/static-page/urlcode.yaml +7 -0
  93. package/recipes/static-plus-api/README.md +6 -0
  94. package/recipes/static-plus-api/urlcode.yaml +4 -0
  95. package/recipes/store-crud/README.md +53 -0
  96. package/recipes/store-crud/recipe.yaml +31 -0
  97. package/recipes/store-crud/tests/requests.json +18 -0
  98. package/recipes/store-crud/urlcode.yaml +18 -0
  99. package/schemas/urlcode.schema.json +125 -60
  100. package/skills/urlcode/SKILL.md +53 -26
  101. package/starters/default/AGENTS.md +43 -43
  102. package/starters/page/README.md +14 -0
  103. package/starters/page/public/index.html +12 -0
  104. package/starters/page/tests/requests.json +17 -0
  105. package/starters/page/urlcode.yaml +6 -0
  106. package/.claude-plugin/marketplace.json +0 -18
  107. package/CONTRIBUTING.md +0 -112
  108. package/ROADMAP.md +0 -66
  109. package/docs/AI-AUTHORING.md +0 -338
  110. package/docs/ASSETS.md +0 -107
  111. package/docs/AUTH-BACKUP.md +0 -32
  112. package/docs/AWS.md +0 -86
  113. package/docs/BEST-PRACTICES.md +0 -276
  114. package/docs/BULK.md +0 -79
  115. package/docs/CAPABILITIES.md +0 -192
  116. package/docs/CAPACITY.md +0 -305
  117. package/docs/CI-FOLLOWUP-2026-09-19.md +0 -97
  118. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +0 -322
  119. package/docs/CI.md +0 -147
  120. package/docs/CLOUDFLARE.md +0 -109
  121. package/docs/CODEBASE-AUDIT-2026-09-20.md +0 -284
  122. package/docs/COMPLIANCE.md +0 -239
  123. package/docs/COMPOSING-A-SITE.md +0 -278
  124. package/docs/CONDITIONS.md +0 -74
  125. package/docs/DEPLOYMENT-CHECKS.md +0 -108
  126. package/docs/DEVELOPMENT-PIPELINE.md +0 -270
  127. package/docs/EGRESS.md +0 -125
  128. package/docs/EXTENSIONS.md +0 -438
  129. package/docs/FRAMEWORK.md +0 -217
  130. package/docs/FUNCTION-SECURITY.md +0 -254
  131. package/docs/HTTP.md +0 -129
  132. package/docs/INSTALL.md +0 -128
  133. package/docs/INTERCHANGE.md +0 -134
  134. package/docs/LOAD-TESTING.md +0 -91
  135. package/docs/LOCAL-DEVELOPMENT.md +0 -102
  136. package/docs/MIDDLEWARE-EXAMPLES.md +0 -75
  137. package/docs/MIDDLEWARE.md +0 -102
  138. package/docs/MONITORING.md +0 -115
  139. package/docs/OBSERVABILITY.md +0 -222
  140. package/docs/OPEN-DECISIONS.md +0 -224
  141. package/docs/OPERATIONAL-PROOF.md +0 -41
  142. package/docs/OPERATIONS.md +0 -201
  143. package/docs/ORGANIZATION.md +0 -135
  144. package/docs/PERFORMANCE.md +0 -72
  145. package/docs/PLUGINS.md +0 -271
  146. package/docs/POLICIES.md +0 -211
  147. package/docs/PRERENDER.md +0 -245
  148. package/docs/PROJECT-DIRECTION.md +0 -118
  149. package/docs/PROVIDER-VERIFICATION.md +0 -84
  150. package/docs/READINESS.md +0 -150
  151. package/docs/README.md +0 -87
  152. package/docs/RECIPES.md +0 -99
  153. package/docs/RELEASE-0.4.0-alpha.3.md +0 -50
  154. package/docs/RELEASE-0.4.1.md +0 -73
  155. package/docs/RELEASE-READINESS.md +0 -117
  156. package/docs/RELEASE-SECURITY.md +0 -96
  157. package/docs/RESILIENCE.md +0 -161
  158. package/docs/ROUTING.md +0 -92
  159. package/docs/SANDBOX-REVIEW.md +0 -72
  160. package/docs/SCAFFOLDING.md +0 -70
  161. package/docs/SECURITY-AUDIT.md +0 -164
  162. package/docs/SITE.md +0 -150
  163. package/docs/SPECIFICATION.md +0 -359
  164. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +0 -288
  165. package/docs/SPIKE-BUSINESS-SUITE.md +0 -1029
  166. package/docs/SPIKE-CORE-LAYERING.md +0 -368
  167. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +0 -211
  168. package/docs/STANDARDS.md +0 -311
  169. package/docs/STARTERS.md +0 -83
  170. package/docs/STATIC.md +0 -105
  171. package/docs/TOOLING.md +0 -298
  172. package/docs/TUNNELS.md +0 -72
  173. package/docs/TYPESCRIPT-AUTHORING.md +0 -87
  174. package/docs/TYPESCRIPT.md +0 -123
  175. package/docs/VERCEL.md +0 -114
  176. package/docs/VERSION-ALIGNMENT.md +0 -80
  177. package/docs/YAML-GUIDE.md +0 -57
  178. package/docs/YAML-REFERENCE.md +0 -449
  179. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +0 -68
  180. package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +0 -102
  181. package/docs/archive/2026-09-19/NEXT-PHASE-PLAN.md +0 -108
  182. package/docs/archive/2026-09-19/NEXT-STEPS.md +0 -646
  183. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +0 -277
  184. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +0 -186
  185. package/docs/archive/2026-09-19/ROADMAP.md +0 -387
  186. package/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md +0 -430
  187. package/docs/archive/2026-09-19/SPIKE-EXTENSIONS.md +0 -492
  188. package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +0 -365
  189. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +0 -778
  190. package/docs/archive/2026-09-19/USABILITY-REVIEW.md +0 -139
  191. package/docs/archive/README.md +0 -28
  192. package/docs/policies/agents.md +0 -182
  193. package/docs/policies/cache.md +0 -152
  194. package/docs/policies/compression.md +0 -169
  195. package/docs/policies/contract.md +0 -52
  196. package/docs/policies/hardened.md +0 -56
  197. package/docs/policies/interoperability.md +0 -169
  198. package/docs/policies/operations.md +0 -45
  199. package/docs/policies/security.md +0 -161
  200. package/docs/policies/throttle.md +0 -103
  201. package/docs/yaml/assets.md +0 -36
  202. package/docs/yaml/conditions.md +0 -20
  203. package/docs/yaml/functions.md +0 -168
  204. package/docs/yaml/middleware.md +0 -31
  205. package/docs/yaml/organization.md +0 -74
  206. package/docs/yaml/policies.md +0 -37
  207. package/docs/yaml/redirects.md +0 -64
  208. package/docs/yaml/responses.md +0 -57
  209. package/docs/yaml/site.md +0 -24
  210. package/packaging/claude-plugin/.claude-plugin/plugin.json +0 -19
  211. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +0 -120
  212. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +0 -108
@@ -1,438 +0,0 @@
1
- # Operator-installed extensions
2
-
3
- Extensions are trusted operator modules, separate from a project's own
4
- `function`/`middleware` code. Auth
5
- and admin implementations live in `urlcode-auth` and `urlcode-admin`; the runtime
6
- supplies only the generic integration contract. No project file can import a host
7
- extension or choose its npm package.
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
-
15
- A project declares versioned configuration and exclusive route mounts:
16
-
17
- ```yaml
18
- version: "1"
19
- extensions:
20
- auth:
21
- version: "1"
22
- config: {}
23
- routes:
24
- /auth/*:
25
- extension: auth
26
- methods: [GET, HEAD, POST]
27
- /private:
28
- respond: {text: Private}
29
- policies:
30
- extensions:
31
- auth: {signedIn: true}
32
- ```
33
-
34
- ## Protecting a route: the `auth` short form
35
-
36
- When the project declares `extensions.auth`, a route may say `auth` instead of
37
- spelling out `policies.extensions.auth`. This is the preferred way to protect a
38
- route:
39
-
40
- ```yaml
41
- routes:
42
- /account:
43
- respond: {text: Account}
44
- auth: {role: member} # or `auth: true` for any signed-in principal
45
- /docs:
46
- respond: {text: Docs}
47
- auth: {required: false} # documents intent; emits no requirement
48
- ```
49
-
50
- The compiler expands the short form before anything else reads the project:
51
- `auth: true` becomes `policies.extensions.auth: {}` and an object becomes the
52
- same object minus `required`. The long form stays the canonical representation,
53
- so `routes`, `audit` and `explain` show the expansion, the extension revision
54
- hash covers it, and the installed auth extension validates the expanded
55
- requirement with its own policy schema. The keys other than `required` are
56
- exactly that schema's keys (`role`, `permission`, `verified`,
57
- `freshWithinSeconds`, `onDeny`); the runtime adds nothing of its own. Loading
58
- fails, naming the route, when `auth` appears without an `extensions.auth`
59
- declaration, next to `policies.extensions.auth`, or next to
60
- `policies.extensions: false`.
61
-
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)).
65
-
66
- The configuration and requirement objects above are validated by the installed
67
- extension's schemas. They are examples of extension-owned fields, not built-in
68
- authentication behavior. See the executable generic fixture in
69
- [examples/extensions](../examples/extensions). Included files can declare
70
- extensions; duplicate names fail rather than silently override one another.
71
-
72
- The operator passes `extensions: RuntimeExtension[]` to `createRuntime`,
73
- `startServer`, or the AWS/Vercel adapter. Types and
74
- `inspectExtensionRevision(project)` are exported from
75
- `@jimhoyd/urlcode/extensions`. Inspection does not grant access: review the
76
- project and place the exact returned SHA-256 in each registration's
77
- `projectSha256`. YAML extension configuration, policies and routes participate
78
- in the revision. Changing them requires an explicit operator reapproval.
79
-
80
- Registrations provide a name, contract version, target list, JSON configuration
81
- schema, optional policy schema, an optional declared `cacheSensitive` (below)
82
- and activation factory. Activation receives the
83
- canonical operator origin, target, revision and mount bases. Its instance handles
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
87
- registrations, stale grants, invalid configuration and unsupported targets fail
88
- activation. Multiple mounts cannot overlap other declared routes.
89
-
90
- For extension-protected routes, agents/throttle run before authorization and
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.
161
-
162
- One exception exists for content-hashed assets. A registration may declare
163
- `immutableAssets: {prefix: '/static'}`, a normalized literal path under each
164
- of its mounts (no `.` or `..` segments, no trailing slash). The runtime then
165
- answers `Cache-Control: public, max-age=31536000, immutable` instead of
166
- no-store only when every condition holds: the request path lies under
167
- `<mount><prefix>/`, the method is GET or HEAD, the status is 200 or 304, the
168
- response carries exactly one strong ETag, sets no Set-Cookie, and does not
169
- vary on Cookie, Authorization or `*`. A stricter Cache-Control the extension
170
- set (no-store, no-cache, private or a shorter max-age) is preserved; other CDN
171
- cache headers are still stripped and compression stays disabled. Anything
172
- that fails a condition, including a cookie added by a later response hook,
173
- stays no-store. The extension owns the content-hashed filename: a file under
174
- the prefix must change its name when its bytes change, because clients never
175
- revalidate it. The prefix belongs to the operator registration, not to the
176
- pinned project revision. The runtime withholds Cookie and Authorization plus any declared
177
- credential headers from all application guest requests and mapped parameters.
178
- This does not isolate browser JavaScript running on the same origin: application
179
- HTML/JS on an authentication origin must be trusted by that site's operator.
180
-
181
- Cloudflare refuses extensions until its artifact format supports their execution.
182
- Node adapter conformance is not a live-provider deployment claim.
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](../packages/auth/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
-
290
- ## Discovering schemas
291
-
292
- Each registration carries the JSON Schemas that validate its `config` block and
293
- its per-route policy requirements. `urlcode extensions` prints them together with
294
- the project's own declarations so an author can see what a mount accepts:
295
-
296
- ```sh
297
- urlcode extensions --project ./site --host-file /absolute/operator/host.mjs [--json]
298
- ```
299
-
300
- For every registration in the host file it reports the name, contract version,
301
- targets, credential headers, configuration schema, policy schema (if any),
302
- whether the project declares it, whether its `projectSha256` matches the current
303
- revision, the routes that mount it and the routes whose policies require it.
304
- Declared names the host does not register are listed as unregistered. The command
305
- executes the trusted host module exactly as `validate` does, including its
306
- absolute-path and outside-project rules, and calls `close` afterwards; it never
307
- activates an extension and grants nothing. Without `--host-file` it lists only
308
- the names the project declares and notes that schemas need the host file.
309
-
310
- The same report is available as `inspectExtensions({project, hostFile?})` from
311
- the package root and, for assistants, as the MCP tool `get_extensions`, which the
312
- server advertises only when the operator started `urlcode mcp` with
313
- `--host-file`. No tool argument can name a host file. See [TOOLING.md](TOOLING.md).
314
-
315
- ## CLI host binding
316
-
317
- Use an explicitly named operator ES module outside the application directory:
318
-
319
- ```sh
320
- urlcode serve --project ./site --origin https://site.example \
321
- --host-file /absolute/operator/host.mjs
322
- ```
323
-
324
- The module default-exports `{extensions, plugins?, close?}`. It may import installed
325
- operator packages, open their stores and read operator secrets. `close` releases
326
- shared services when the CLI command finishes or the server shuts down. A runtime
327
- reload closes extension instances but does not close caller-owned services. Host
328
- modules are not watched or automatically rediscovered. Restart to update them.
329
-
330
- The same explicit option is supported by dev, validate, test, routes, audit,
331
- benchmark, extensions and mcp. These commands execute trusted host activation and may access its
332
- store; read-only project inspection commands never implicitly load a host file.
333
- Host-file paths must be absolute `.mjs`/`.js` files whose real path lies outside
334
- the project, including after symlink resolution. This is an operator-code trust
335
- boundary, not a JavaScript sandbox or an independent security review.
336
-
337
- ## Scaffolding with `init --with`
338
-
339
- `urlcode init <directory> --with ui,auth,admin` produces the layered site the
340
- [framework page](FRAMEWORK.md#the-composition-contract) describes in one
341
- command: the starter under `<directory>/app/`, one `host.mjs`, one `README.md`,
342
- and each extension's own operator files. Core never bundles or imports the
343
- extension packages at build time; at run time it resolves
344
- `@jimhoyd/urlcode-<name>` for each name with Node's package resolution from
345
- the invoking directory. Install a compatible package set there, as shown in
346
- [the framework guide](FRAMEWORK.md#the-composition-contract), before using
347
- `--with ui,auth`; UI must activate before auth. Core imports the package and calls its
348
- `scaffold` export with this request:
349
-
350
- ```ts
351
- interface ScaffoldRequest {
352
- directory: string; // absolute site directory; result file paths are relative to it
353
- project: string; // absolute route project, <directory>/app (holds urlcode.yaml)
354
- hostFile: string; // absolute combined host module, <directory>/host.mjs
355
- names: readonly string[]; // every name in --with order, including this one
356
- }
357
- interface ScaffoldFile { path: string; content: string | Uint8Array; mode?: number }
358
- interface ScaffoldResult {
359
- name: string; // must equal the requested name
360
- extensions: Record<string, unknown>; // merged into the project's top-level extensions
361
- routes: Record<string, unknown>; // merged into app/routes/extensions.yaml
362
- hostImports: string[]; hostSetup: string[]; hostEntries: string[]; hostClose?: string[];
363
- files: ScaffoldFile[]; // written relative to directory with their modes
364
- readme: string; nextSteps: string[]; // README section and numbered steps
365
- env?: Record<string, string>; // environment variables the host reads
366
- }
367
- ```
368
-
369
- `scaffold` writes nothing; it returns fragments and may generate key material
370
- in memory (core zeroes `Uint8Array` contents after writing or on failure). The
371
- types are exported from `@jimhoyd/urlcode` for packages that want to typecheck
372
- against them.
373
-
374
- Assembly rules, in `--with` order:
375
-
376
- - Every package is resolved and every `scaffold` is called before anything is
377
- written. A name that is not installed refuses with the `npm install` command;
378
- a package without a `scaffold` export refuses and names the package; an error
379
- thrown by a `scaffold` (for example admin without auth in the same `--with`)
380
- is reported as that package's refusal. No directory is left behind.
381
- - `extensions` fragments are declared in `app/urlcode.yaml`; `routes`
382
- fragments are written to `app/routes/extensions.yaml`, appended to the
383
- starter's `includes`, so the starter's own routes load first. A route or
384
- extension key produced twice, or one the starter already declares, is refused
385
- naming both sources.
386
- - `host.mjs` is all `hostImports`, then all `hostSetup` lines, then an
387
- `extensions` array of every `hostEntries` item, then `close()` running the
388
- `hostClose` statements in reverse `--with` order so later entries release
389
- before what they built on. Setup lines share one module scope: admin's entry
390
- references the `service`, `csrfKey` and `projectSha256` identifiers that
391
- auth's setup defines, which is why `names` carries the full list.
392
- - `files` are created exclusively (`wx`) with their `mode` (default `0644`),
393
- must stay inside the site directory and outside `app/`, and never pass
394
- through a symlink. Nothing generated is ever overwritten; an existing
395
- destination refuses like plain `init`.
396
- - `README.md` holds the starter's README as a section, then each result's
397
- `readme` under `## Extension: <name>`, the merged numbered `nextSteps`, the
398
- merged `env` table and the project revision. The command prints that
399
- revision (`inspectExtensionRevision` of `app/`) with the instruction to
400
- review the project and pin it explicitly; the host is generated to require
401
- the pin, never to compute it.
402
-
403
- ### Recorded versions
404
-
405
- `init --with` also writes `<directory>/package.json`: a private manifest that
406
- pins, at exactly the version that was resolved, the running runtime, every
407
- extension named in `--with`, and every package those extensions declare in
408
- `peerDependencies` (so `@jimhoyd/urlcode-ui` is pinned for an `auth,admin` site
409
- although nobody named it). Before anything is written, the whole set is checked
410
- against every declared peer range; an incompatible combination or a missing
411
- required peer refuses and names it, leaving no directory behind.
412
-
413
- Nothing is installed. The generated site has no `node_modules` and no
414
- `package-lock.json` until you run `npm install` in it yourself, which the
415
- command and the generated README both state as the next step. Reproducibility
416
- comes from that install, not from generation.
417
-
418
- - `--no-manifest` generates the site without a `package.json`, for a site whose
419
- dependencies are managed elsewhere. Plain `urlcode init` is unchanged and
420
- still writes no manifest; add `--manifest` to pin the runtime for a
421
- route-only project too.
422
- - `--pin <package>=<specifier>` records a specifier instead of the resolved
423
- version, for local tarball or offline development
424
- (`--pin @jimhoyd/urlcode-auth=file:/abs/urlcode-auth-0.1.0-alpha.6.tgz`). A
425
- package installed from a local path or tarball is detected from npm's own
426
- install record and pinned by that path without any flag; the README says so,
427
- because such a pin only reproduces where that path exists.
428
-
429
- There is no upgrade command. Moving a generated project to newer versions today
430
- means editing its `package.json` and re-running `npm install` yourself; nothing
431
- in this runtime selects a newer tested set, shows the change, or updates a
432
- lockfile for you.
433
-
434
- Serving the result is the usual explicit host binding:
435
-
436
- ```sh
437
- urlcode validate --project app --host-file "$PWD/host.mjs" --origin https://site.example
438
- ```
package/docs/FRAMEWORK.md DELETED
@@ -1,217 +0,0 @@
1
- # The URLCode framework
2
-
3
- One page for people and AI agents. It says what the four packages are, how a
4
- project grows from a handful of redirects into an application with accounts
5
- and an administration console, and which facts an agent must not guess. Every
6
- claim here is implemented in the linked repository; nothing is roadmap.
7
-
8
- ## Four packages, one project shape
9
-
10
- | Package | Source | What it adds | How a project declares it |
11
- |---|---|---|---|
12
- | `@jimhoyd/urlcode` | this repository | 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
- | `@jimhoyd/urlcode-ui` | [`packages/ui`](../packages/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
- | `@jimhoyd/urlcode-auth` | [`packages/auth`](../packages/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
- | `@jimhoyd/urlcode-admin` | [`packages/admin`](../packages/admin) | Administration: users, sessions, roles, audit, registration approval, two-person cases, support impersonation, health | `extensions.admin` plus an `/admin/*` mount |
16
-
17
- All four are Apache-2.0. The `0.4.1` release line aligns their stable versions;
18
- check `npm run release:status` for publication progress. A stable npm channel is
19
- not an independent assessment: review, deployment evidence and an accessibility
20
- assessment are still pending
21
- ([issue 58](https://github.com/jimhoyd-com/urlcode/issues/58)). Their status
22
- files say exactly what is built: [auth](../packages/auth/IMPLEMENTATION-STATUS.md),
23
- [admin](../packages/admin/IMPLEMENTATION-STATUS.md),
24
- [ui](../packages/ui/IMPLEMENTATION-STATUS.md).
25
- The current version of each package is its own manifest, and the peer ranges it
26
- declares are in that manifest too; do not read a version number out of this
27
- page. How versions, channels and release tags line up is recorded in
28
- [package and channel alignment](VERSION-ALIGNMENT.md), and `npm run
29
- release:status` reports the live registry and tag state.
30
-
31
- ## The ladder
32
-
33
- A project climbs these rungs by adding YAML, never by rewriting what it has.
34
- Each rung's YAML is valid on every rung above it.
35
-
36
- 1. **Redirects.** A `urlcode.yaml` with `redirect` routes. No code, no database,
37
- runs anywhere, including Vercel, AWS Lambda and Cloudflare Workers.
38
- Thousands of rows import from CSV or provider files with `bulk-import`.
39
- 2. **Responses, pages and files.** `respond`, `page`, `static` and `download`
40
- handlers, `site` conventions (robots, sitemap, favicon, security.txt,
41
- llms.txt) and `policies` (throttle, agents, security headers, compression,
42
- cache). Still no code.
43
- 3. **Functions and middleware.** `function` routes and ordered `middleware`
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). The `env`/`secrets` the
47
- runtime injects into a function come only from an operator grant pinned to
48
- the project revision; the grant governs that injected context, not the
49
- ambient Node environment trusted in-process code can reach on its own.
50
- 4. **Accounts.** The `auth` extension: sign-in, registration, MFA, account
51
- page and protected routes. The operator installs it in a host file outside
52
- the project; YAML only declares the mount and configuration.
53
- 5. **Administration.** The `admin` extension on the same service: manage the
54
- people who signed up, their sessions and roles, review the audit trail.
55
-
56
- 6. **Your own look.** A shared `presentation` (catalogue and theme variables)
57
- restyles auth and admin together; the `ui` extension adds the template kit,
58
- project copy, template and stylesheet overrides for kit-rendered pages.
59
-
60
- Stored short links previously sat here as a native `link` route; that handler
61
- was removed from core. A `urlcode-dynamic-link` package owned them the same way
62
- `auth`/`admin` own their mounts, but it has been retired and unpublished; no
63
- package occupies this rung today.
64
-
65
- Rungs 1 to 3 need only the core package. Rungs 4 to 6 need the extension
66
- packages installed from npm and a Node host with a
67
- patched SQLite build; see each package's README ([auth](../packages/auth/README.md),
68
- [admin](../packages/admin/README.md), [ui](../packages/ui/README.md)) for the
69
- exact requirement.
70
-
71
- ## The composition contract
72
-
73
- After the aligned `0.4.1` packages are published, an extended project starts
74
- with the packages and one command:
75
-
76
- ```sh
77
- npm install --save-exact @jimhoyd/urlcode@0.4.1 @jimhoyd/urlcode-ui@0.4.1 @jimhoyd/urlcode-auth@0.4.1 @jimhoyd/urlcode-admin@0.4.1
78
- urlcode init my-site --with ui,auth,admin
79
- ```
80
-
81
- Installing from npm is the normal path; `scripts/pack-sources.mjs` still builds
82
- local tarballs from a reviewed checkout for operators who install only source
83
- they have read — one revision now covers core and every extension. Three files make an extended
84
- project. Nothing else is discovered by convention.
85
-
86
- ```
87
- site/
88
- urlcode.yaml the project: routes, extensions, policies (Git-owned, untrusted content)
89
- functions/, public/ guest code and assets referenced from the YAML
90
- operator/
91
- host.mjs trusted operator code: default-exports { extensions, plugins?, close? }
92
- operator-service.mjs opens the auth store, keys and senders; imported by host.mjs
93
- data/ private: auth.sqlite, encryption key, CSRF key
94
- ```
95
-
96
- The project declares logical extensions and exclusive mounts:
97
-
98
- ```yaml
99
- version: "1"
100
- extensions:
101
- ui: { version: "1", config: { theme: { name: Acme, colors: { primary: "24 95% 53%" } } } }
102
- auth: { version: "1", config: { registration: "off" } }
103
- admin: { version: "1", config: {} }
104
- routes:
105
- /assets/ui/*: { extension: ui, methods: [GET, HEAD] }
106
- /account/*: { extension: auth, methods: [GET, HEAD, POST] }
107
- /admin/*: { extension: admin, methods: [GET, HEAD, POST] }
108
- /private:
109
- respond: { text: Signed in }
110
- policies:
111
- extensions:
112
- auth: {}
113
- ```
114
-
115
- The operator host explicitly registers the packages. Registration is an
116
- activation boundary; it does not isolate trusted application code from the host:
117
-
118
- ```js
119
- import { createUiExtension } from '@jimhoyd/urlcode-ui/host';
120
- import { authExtension, createPresentation, englishCatalogue } from '@jimhoyd/urlcode-auth';
121
- import { adminExtension } from '@jimhoyd/urlcode-admin';
122
- import { service, csrfKey, projectSha256 } from './operator-service.mjs';
123
-
124
- const ui = createUiExtension({ projectSha256, projectRoot: '/absolute/site', sources: [englishCatalogue] });
125
- const presentation = createPresentation({ theme: { '--ui-accent': '#0645ad' } });
126
- export default {
127
- extensions: [
128
- ui.registration,
129
- authExtension({ service, csrfKey, projectSha256, presentation }),
130
- adminExtension({ service, csrfKey, projectSha256, authMount: '/account', presentation }),
131
- ],
132
- async close() { await service.close(); },
133
- };
134
- ```
135
-
136
- Auth and admin already render their screens through `ui.kit` when the host
137
- supplies the UI extension; their package-owned templates and catalogues must be
138
- registered with that kit. Without it, both retain shared primitive rendering
139
- through `presentation`. The example above uses that primitive fallback. The UI
140
- block is optional, and a migration to the kit is not unfinished framework work.
141
- See each package README for its complete kit registration example.
142
-
143
- ```sh
144
- urlcode serve --project /absolute/site --host-file /absolute/operator/host.mjs --origin https://site.example
145
- ```
146
-
147
- `urlcode init <dir> --with ui,auth,admin` writes this layout in one step: it
148
- resolves each installed `@jimhoyd/urlcode-<name>` from the current directory,
149
- calls its `scaffold` export and merges the fragments into `app/urlcode.yaml`,
150
- one `host.mjs` and one `README.md`, refusing before writing anything when a
151
- package is missing or two fragments collide (the contract is documented under
152
- [scaffolding](EXTENSIONS.md#scaffolding-with-init---with)). `urlcode-auth init`
153
- and `urlcode-admin init` write the same layout for a single package; `urlcode-auth bootstrap` creates the first
154
- administrator from JSON on stdin. `inspectExtensionRevision(project)` prints
155
- the SHA-256 that `projectSha256` must carry; changing extension YAML, policies
156
- or mounts changes the revision and needs an explicit operator reapproval.
157
-
158
- The presentation tooling composes the same way, by naming packages rather than
159
- depending on them. `urlcode-ui` is the kit alone until
160
- `--extensions @jimhoyd/urlcode-auth,@jimhoyd/urlcode-admin` names the packages
161
- that ship the other namespaces: each is resolved from `--project` with Node
162
- package resolution and imported for the namespace it exports, so `list`,
163
- `doctor`, `eject`, `preview` and `copy --missing` cover the `auth/*` and
164
- `admin/*` templates and copy the host registers, and a project override of an
165
- extension template is checked against the shipped view model. A package that is
166
- not installed is skipped. `urlcode init --with` writes the commands with the
167
- flag already set; `@jimhoyd/urlcode-ui` still depends on neither peer.
168
-
169
- ## Rules an agent must follow
170
-
171
- These are the facts that keep generated projects valid. The full matrix is in
172
- [AI authoring](AI-AUTHORING.md); this is the short list.
173
-
174
- - **Extension YAML names logical extensions, not host packages or credentials.**
175
- Function and middleware `source` fields do name project modules. Extensions are
176
- logical names; the host file chooses the implementation. There is no
177
- `--extension` flag, no `import` in YAML, no interpolation.
178
- - **One handler per route.** `redirect`, `respond`, `page`, `static`, `download`,
179
- `function`, `proxy`, `conditional` or `extension`, plus optional
180
- `middleware`. Paths are exact or single-segment `{param}`; `/*` only on
181
- `static` and `extension` mounts. No regex.
182
- - **`function`/`middleware` code is trusted by default, sandboxed opt-in.**
183
- It runs in-process with full Node access unless the route declares
184
- `sandbox: true`, which isolates it to a text/JSON `Request`/`Response`
185
- subset, validated `args` and granted `env`, with no `fetch`, Node,
186
- filesystem or general network access; bounded timers are available. Either way, `args`/`env`/`secrets` are exactly what
187
- the route declares and an operator grants — trust changes where code runs,
188
- not what it is handed. See docs/SPIKE-DEFAULT-TRUST-MODEL.md and
189
- docs/FUNCTION-SECURITY.md.
190
- - **Authentication is host processing.** Do not build login forms, session
191
- cookies or password checks in functions. With the auth extension declared,
192
- prefer `auth: true` or `auth: {role: admin}`; these expand to
193
- `policies.extensions.auth`. The runtime filters credential headers passed to
194
- application handlers. This is not a security boundary against trusted Node code.
195
- - **Everything is validated before it runs.** `urlcode validate --local`,
196
- `urlcode test`, `urlcode audit --expect-routes N`. Unsupported features fail
197
- with the route named; nothing degrades silently.
198
- - **Provider targets refuse what they cannot enforce.** Cloudflare runs
199
- redirects and declared responses only. Serverless adapters refuse functions,
200
- proxy, signals and extensions. The `static` target (S3 + CloudFront,
201
- no server) refuses everything that needs request-time logic, keeping only
202
- `redirect`/`respond`/`page`/`static`/`download` — see [static
203
- hosting](STATIC.md). Check `urlcode capabilities --target NAME` before
204
- promising a deployment.
205
- - **Report evidence, not hope.** The commands above are the evidence. Local
206
- tests are not deployment, soak or independent security review.
207
-
208
- ## Where to read next
209
-
210
- | Need | Read |
211
- |---|---|
212
- | Write or change routes | [YAML guide](YAML-GUIDE.md), [field reference](YAML-REFERENCE.md), [cookbook](../examples/cookbook/README.md) |
213
- | Add accounts | [auth README](../packages/auth/README.md), [auth security](../packages/auth/SECURITY.md) |
214
- | Add administration | [admin README](../packages/admin/README.md) |
215
- | Restyle every page | [ui README](../packages/ui/README.md), [ui contract](../packages/ui/CONTRACT.md) |
216
- | Write an extension | [extensions](EXTENSIONS.md), [extension model review](archive/2026-09-19/SPIKE-EXTENSION-MODEL.md) |
217
- | Run it | [operations](OPERATIONS.md), [install](INSTALL.md), [deployment checks](DEPLOYMENT-CHECKS.md) |