@jimhoyd/urlcode 0.4.2 → 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 (214) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +57 -14
  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 +30 -27
  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 +28 -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 +60 -2
  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/project-tests.js +35 -11
  25. package/dist/readiness.js +184 -32
  26. package/dist/recipes.js +1 -1
  27. package/dist/router.js +17 -0
  28. package/dist/runtime.js +26 -3
  29. package/dist/scaffold.js +0 -0
  30. package/dist/server.js +26 -3
  31. package/dist/site.js +0 -0
  32. package/dist/tooling.js +2 -1
  33. package/dist/types/agent-context.d.ts +44 -0
  34. package/dist/types/authoring.d.ts +3 -1
  35. package/dist/types/body-schema.d.ts +58 -0
  36. package/dist/types/config.d.ts +10 -2
  37. package/dist/types/context.d.ts +1 -1
  38. package/dist/types/errors.d.ts +9 -1
  39. package/dist/types/examples.d.ts +1 -1
  40. package/dist/types/extensions.d.ts +47 -2
  41. package/dist/types/http-policy.d.ts +3 -0
  42. package/dist/types/init-with.d.ts +10 -1
  43. package/dist/types/pattern-guard.d.ts +10 -0
  44. package/dist/types/project-tests.d.ts +9 -0
  45. package/dist/types/readiness.d.ts +68 -0
  46. package/dist/types/recipes.d.ts +1 -1
  47. package/dist/types/runtime.d.ts +4 -0
  48. package/dist/types/server.d.ts +9 -1
  49. package/dist/types/tooling.d.ts +1 -0
  50. package/dist/types/types.d.ts +13 -0
  51. package/dist/types.js +10 -2
  52. package/dist/typescript-authoring.js +5 -3
  53. package/dist/verify-deployment.js +16 -10
  54. package/examples/body-validation/README.md +16 -0
  55. package/examples/body-validation/example.yaml +17 -0
  56. package/examples/body-validation/tests/requests.json +15 -0
  57. package/examples/body-validation/urlcode.yaml +34 -0
  58. package/examples/coverage-waiver/README.md +8 -0
  59. package/examples/coverage-waiver/example.yaml +16 -0
  60. package/examples/coverage-waiver/functions/notes.mjs +2 -0
  61. package/examples/coverage-waiver/tests/requests.json +3 -0
  62. package/examples/coverage-waiver/urlcode.yaml +10 -0
  63. package/examples/data-dir/README.md +39 -0
  64. package/examples/data-dir/data/welcome.txt +1 -0
  65. package/examples/data-dir/example.yaml +22 -0
  66. package/examples/data-dir/functions/note.mjs +18 -0
  67. package/examples/data-dir/tests/requests.json +6 -0
  68. package/examples/data-dir/urlcode.yaml +6 -0
  69. package/examples/lifecycle/README.md +15 -0
  70. package/examples/lifecycle/example.yaml +19 -0
  71. package/examples/lifecycle/functions/notes.mjs +29 -0
  72. package/examples/lifecycle/tests/requests.json +12 -0
  73. package/examples/lifecycle/urlcode.yaml +29 -0
  74. package/examples/not-found/README.md +10 -0
  75. package/examples/not-found/example.yaml +17 -0
  76. package/examples/not-found/public/404.html +3 -0
  77. package/examples/not-found/public/index.html +3 -0
  78. package/examples/not-found/tests/requests.json +7 -0
  79. package/examples/not-found/urlcode.yaml +7 -0
  80. package/examples/shared-blocks/README.md +11 -0
  81. package/examples/shared-blocks/example.yaml +17 -0
  82. package/examples/shared-blocks/tests/requests.json +7 -0
  83. package/examples/shared-blocks/urlcode.yaml +37 -0
  84. package/llms-full.txt +364 -49
  85. package/llms.txt +43 -5
  86. package/package.json +15 -22
  87. package/recipes/static-page/README.md +9 -0
  88. package/recipes/static-page/public/index.html +11 -0
  89. package/recipes/static-page/recipe.yaml +21 -0
  90. package/recipes/static-page/tests/requests.json +22 -0
  91. package/recipes/static-page/urlcode.yaml +7 -0
  92. package/recipes/static-plus-api/README.md +6 -0
  93. package/recipes/static-plus-api/urlcode.yaml +4 -0
  94. package/recipes/store-crud/README.md +53 -0
  95. package/recipes/store-crud/recipe.yaml +31 -0
  96. package/recipes/store-crud/tests/requests.json +18 -0
  97. package/recipes/store-crud/urlcode.yaml +18 -0
  98. package/schemas/urlcode.schema.json +125 -60
  99. package/skills/urlcode/SKILL.md +47 -27
  100. package/starters/default/AGENTS.md +43 -43
  101. package/starters/page/README.md +14 -0
  102. package/starters/page/public/index.html +12 -0
  103. package/starters/page/tests/requests.json +17 -0
  104. package/starters/page/urlcode.yaml +6 -0
  105. package/.claude-plugin/marketplace.json +0 -18
  106. package/CONTRIBUTING.md +0 -148
  107. package/ROADMAP.md +0 -70
  108. package/docs/AI-AUTHORING.md +0 -339
  109. package/docs/ASSETS.md +0 -107
  110. package/docs/AUTH-BACKUP.md +0 -32
  111. package/docs/AWS.md +0 -86
  112. package/docs/BEST-PRACTICES.md +0 -276
  113. package/docs/BULK.md +0 -79
  114. package/docs/CAPABILITIES.md +0 -192
  115. package/docs/CAPACITY.md +0 -305
  116. package/docs/CI-FOLLOWUP-2026-09-19.md +0 -97
  117. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +0 -322
  118. package/docs/CI.md +0 -147
  119. package/docs/CLOUDFLARE.md +0 -109
  120. package/docs/CODEBASE-AUDIT-2026-09-20.md +0 -284
  121. package/docs/COMPLIANCE.md +0 -239
  122. package/docs/COMPOSING-A-SITE.md +0 -287
  123. package/docs/CONDITIONS.md +0 -74
  124. package/docs/CONTAINER-PROMOTION.md +0 -74
  125. package/docs/DEPLOYMENT-CHECKS.md +0 -108
  126. package/docs/DEVELOPMENT-PIPELINE.md +0 -304
  127. package/docs/EGRESS.md +0 -125
  128. package/docs/EXTENSIONS.md +0 -403
  129. package/docs/FRAMEWORK.md +0 -217
  130. package/docs/FUNCTION-SECURITY.md +0 -251
  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 -149
  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 -153
  151. package/docs/README.md +0 -93
  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-0.4.2.md +0 -30
  156. package/docs/RELEASE-READINESS.md +0 -146
  157. package/docs/RELEASE-SECURITY.md +0 -100
  158. package/docs/RESILIENCE.md +0 -161
  159. package/docs/ROUTING.md +0 -92
  160. package/docs/SANDBOX-REVIEW.md +0 -72
  161. package/docs/SCAFFOLDING.md +0 -70
  162. package/docs/SECURITY-AUDIT.md +0 -164
  163. package/docs/SITE.md +0 -150
  164. package/docs/SPECIFICATION.md +0 -359
  165. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +0 -288
  166. package/docs/SPIKE-BUSINESS-SUITE.md +0 -1029
  167. package/docs/SPIKE-CORE-LAYERING.md +0 -368
  168. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +0 -207
  169. package/docs/STANDARDS.md +0 -311
  170. package/docs/STARTERS.md +0 -83
  171. package/docs/STATIC.md +0 -105
  172. package/docs/TOOLING.md +0 -298
  173. package/docs/TUNNELS.md +0 -72
  174. package/docs/TYPESCRIPT-AUTHORING.md +0 -87
  175. package/docs/TYPESCRIPT.md +0 -123
  176. package/docs/VERCEL.md +0 -114
  177. package/docs/VERSION-ALIGNMENT.md +0 -88
  178. package/docs/YAML-GUIDE.md +0 -57
  179. package/docs/YAML-REFERENCE.md +0 -449
  180. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +0 -68
  181. package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +0 -102
  182. package/docs/archive/2026-09-19/NEXT-PHASE-PLAN.md +0 -108
  183. package/docs/archive/2026-09-19/NEXT-STEPS.md +0 -646
  184. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +0 -277
  185. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +0 -186
  186. package/docs/archive/2026-09-19/ROADMAP.md +0 -387
  187. package/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md +0 -430
  188. package/docs/archive/2026-09-19/SPIKE-EXTENSIONS.md +0 -492
  189. package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +0 -365
  190. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +0 -778
  191. package/docs/archive/2026-09-19/USABILITY-REVIEW.md +0 -139
  192. package/docs/archive/2026-09-20/OPEN-DECISIONS-COMPLETED.md +0 -116
  193. package/docs/archive/README.md +0 -29
  194. package/docs/policies/agents.md +0 -182
  195. package/docs/policies/cache.md +0 -152
  196. package/docs/policies/compression.md +0 -169
  197. package/docs/policies/contract.md +0 -52
  198. package/docs/policies/hardened.md +0 -56
  199. package/docs/policies/interoperability.md +0 -169
  200. package/docs/policies/operations.md +0 -45
  201. package/docs/policies/security.md +0 -161
  202. package/docs/policies/throttle.md +0 -103
  203. package/docs/yaml/assets.md +0 -36
  204. package/docs/yaml/conditions.md +0 -20
  205. package/docs/yaml/functions.md +0 -168
  206. package/docs/yaml/middleware.md +0 -31
  207. package/docs/yaml/organization.md +0 -74
  208. package/docs/yaml/policies.md +0 -37
  209. package/docs/yaml/redirects.md +0 -64
  210. package/docs/yaml/responses.md +0 -57
  211. package/docs/yaml/site.md +0 -24
  212. package/packaging/claude-plugin/.claude-plugin/plugin.json +0 -19
  213. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +0 -130
  214. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +0 -108
@@ -1,403 +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
- Extensions expose project customization points through the core hook primitive.
187
- Each registration publishes `hooks`, a machine-readable list containing the
188
- hook name, whether it is a value-transforming `filter` or side-effect `action`,
189
- its description and its input/output JSON Schemas. The extension embeds
190
- `extensionHooksSchema(contracts)` in its configuration schema and calls
191
- `loadExtensionHooks(config.hooks, contracts, context)` during activation.
192
- Core then enforces the common source/export shape, project-root confinement,
193
- known names, eager module/export validation, input/output schemas and reload
194
- cache busting. Hook entry bytes participate in the project revision, so editing
195
- a hook invalidates the operator's extension pin.
196
-
197
- Projects select those declared hooks in the extension's own configuration:
198
-
199
- ```yaml
200
- extensions:
201
- auth:
202
- version: "1"
203
- config:
204
- hooks:
205
- beforeRegister:
206
- source: ./hooks/registration-rule.mjs
207
- export: default
208
- onSignUp:
209
- source: ./hooks/on-signup.mjs
210
- ```
211
-
212
- with `beforeRegister` called before an account is created, given a typed
213
- `{email, profile?}` input and returning a typed verdict (`{allow: true}`
214
- or `{allow: false, reason}`), and `onSignUp` called after, for side effects
215
- such as provisioning a workspace. Hook names and lifecycle timing remain the
216
- extension's domain, while their declaration, loading and discovery are shared.
217
-
218
- Hooks are first-party project code and run trusted in-process by default, with
219
- full Node access, like trusted `function` and `middleware` routes. Contract v1
220
- does not define an arbitrary-value sandbox hook protocol. A hook reference with
221
- `sandbox: true` is rejected during activation rather than silently run trusted.
222
- Only the entry module is refreshed during reactivation; its imported dependencies
223
- remain in Node's module cache until restart.
224
-
225
- The UI extension exposes `transformView`, a synchronous filter called before a
226
- named kit template renders. It receives `{template, view}` and returns the view
227
- model to render. Use copy, templates, theme and CSS for ordinary presentation
228
- changes; use this hook for project-specific computed view data that those
229
- declarative layers cannot express.
230
-
231
- ## Building an extension
232
-
233
- An extension package should export a registration factory and, when it supports
234
- `urlcode init --with`, a side-effect-free `scaffold` function. The registration:
235
-
236
- 1. Declares its logical name, contract version, supported targets, exact project
237
- revision pin and strict configuration/policy schemas.
238
- 2. Publishes every project hook through `hooks` and reuses
239
- `extensionHooksSchema` plus `loadExtensionHooks`; it does not implement its
240
- own path resolver or dynamic-import cache.
241
- 3. Activates all configuration, files, services and hooks before serving a
242
- request. Invalid or stale configuration fails activation.
243
- 4. Returns `handle` for mounts and optionally `authorize`/`middleware` for route
244
- policies. It closes resources it owns.
245
- 5. Keeps credentials, storage and provider setup in the operator host. Project
246
- YAML contains logical configuration and project-relative hook references.
247
-
248
- Consumers install the package, declare its YAML block and mounts/policies, and
249
- register it in `host.mjs`. They modify it through declared configuration,
250
- presentation layers and hooks. A fork is reserved for changing behavior the
251
- extension has not exposed; that is evidence for a new declarative field or hook.
252
- See [Composing a site](COMPOSING-A-SITE.md) for the complete ui/auth/admin example.
253
-
254
- ## Discovering schemas
255
-
256
- Each registration carries the JSON Schemas that validate its `config` block and
257
- its per-route policy requirements, plus its hook contracts. `urlcode extensions` prints them together with
258
- the project's own declarations so an author can see what a mount accepts:
259
-
260
- ```sh
261
- urlcode extensions --project ./site --host-file /absolute/operator/host.mjs [--json]
262
- ```
263
-
264
- For every registration in the host file it reports the name, contract version,
265
- targets, credential headers, configuration schema, policy schema (if any),
266
- declared hook names, kinds, descriptions and input/output schemas,
267
- whether the project declares it, whether its `projectSha256` matches the current
268
- revision, the routes that mount it and the routes whose policies require it.
269
- Declared names the host does not register are listed as unregistered. The command
270
- executes the trusted host module exactly as `validate` does, including its
271
- absolute-path and outside-project rules, and calls `close` afterwards; it never
272
- activates an extension and grants nothing. Without `--host-file` it lists only
273
- the names the project declares and notes that schemas need the host file.
274
-
275
- The same report is available as `inspectExtensions({project, hostFile?})` from
276
- the package root and, for assistants, as the MCP tool `get_extensions`, which the
277
- server advertises only when the operator started `urlcode mcp` with
278
- `--host-file`. No tool argument can name a host file. See [TOOLING.md](TOOLING.md).
279
-
280
- ## CLI host binding
281
-
282
- Use an explicitly named operator ES module outside the application directory:
283
-
284
- ```sh
285
- urlcode serve --project ./site --origin https://site.example \
286
- --host-file /absolute/operator/host.mjs
287
- ```
288
-
289
- The module default-exports `{extensions, plugins?, close?}`. It may import installed
290
- operator packages, open their stores and read operator secrets. `close` releases
291
- shared services when the CLI command finishes or the server shuts down. A runtime
292
- reload closes extension instances but does not close caller-owned services. Host
293
- modules are not watched or automatically rediscovered. Restart to update them.
294
-
295
- The same explicit option is supported by dev, validate, test, routes, audit,
296
- benchmark, extensions and mcp. These commands execute trusted host activation and may access its
297
- store; read-only project inspection commands never implicitly load a host file.
298
- Host-file paths must be absolute `.mjs`/`.js` files whose real path lies outside
299
- the project, including after symlink resolution. This is an operator-code trust
300
- boundary, not a JavaScript sandbox or an independent security review.
301
-
302
- ## Scaffolding with `init --with`
303
-
304
- `urlcode init <directory> --with ui,auth,admin` produces the layered site the
305
- [framework page](FRAMEWORK.md#the-composition-contract) describes in one
306
- command: the starter under `<directory>/app/`, one `host.mjs`, one `README.md`,
307
- and each extension's own operator files. Core never bundles or imports the
308
- extension packages at build time; at run time it resolves
309
- `@jimhoyd/urlcode-<name>` for each name with Node's package resolution from
310
- the invoking directory. Install a compatible package set there, as shown in
311
- [the framework guide](FRAMEWORK.md#the-composition-contract), before using
312
- `--with ui,auth`; UI must activate before auth. Core imports the package and calls its
313
- `scaffold` export with this request:
314
-
315
- ```ts
316
- interface ScaffoldRequest {
317
- directory: string; // absolute site directory; result file paths are relative to it
318
- project: string; // absolute route project, <directory>/app (holds urlcode.yaml)
319
- hostFile: string; // absolute combined host module, <directory>/host.mjs
320
- names: readonly string[]; // every name in --with order, including this one
321
- }
322
- interface ScaffoldFile { path: string; content: string | Uint8Array; mode?: number }
323
- interface ScaffoldResult {
324
- name: string; // must equal the requested name
325
- extensions: Record<string, unknown>; // merged into the project's top-level extensions
326
- routes: Record<string, unknown>; // merged into app/routes/extensions.yaml
327
- hostImports: string[]; hostSetup: string[]; hostEntries: string[]; hostClose?: string[];
328
- files: ScaffoldFile[]; // written relative to directory with their modes
329
- readme: string; nextSteps: string[]; // README section and numbered steps
330
- env?: Record<string, string>; // environment variables the host reads
331
- }
332
- ```
333
-
334
- `scaffold` writes nothing; it returns fragments and may generate key material
335
- in memory (core zeroes `Uint8Array` contents after writing or on failure). The
336
- types are exported from `@jimhoyd/urlcode` for packages that want to typecheck
337
- against them.
338
-
339
- Assembly rules, in `--with` order:
340
-
341
- - Every package is resolved and every `scaffold` is called before anything is
342
- written. A name that is not installed refuses with the `npm install` command;
343
- a package without a `scaffold` export refuses and names the package; an error
344
- thrown by a `scaffold` (for example admin without auth in the same `--with`)
345
- is reported as that package's refusal. No directory is left behind.
346
- - `extensions` fragments are declared in `app/urlcode.yaml`; `routes`
347
- fragments are written to `app/routes/extensions.yaml`, appended to the
348
- starter's `includes`, so the starter's own routes load first. A route or
349
- extension key produced twice, or one the starter already declares, is refused
350
- naming both sources.
351
- - `host.mjs` is all `hostImports`, then all `hostSetup` lines, then an
352
- `extensions` array of every `hostEntries` item, then `close()` running the
353
- `hostClose` statements in reverse `--with` order so later entries release
354
- before what they built on. Setup lines share one module scope: admin's entry
355
- references the `service`, `csrfKey` and `projectSha256` identifiers that
356
- auth's setup defines, which is why `names` carries the full list.
357
- - `files` are created exclusively (`wx`) with their `mode` (default `0644`),
358
- must stay inside the site directory and outside `app/`, and never pass
359
- through a symlink. Nothing generated is ever overwritten; an existing
360
- destination refuses like plain `init`.
361
- - `README.md` holds the starter's README as a section, then each result's
362
- `readme` under `## Extension: <name>`, the merged numbered `nextSteps`, the
363
- merged `env` table and the project revision. The command prints that
364
- revision (`inspectExtensionRevision` of `app/`) with the instruction to
365
- review the project and pin it explicitly; the host is generated to require
366
- the pin, never to compute it.
367
-
368
- ### Recorded versions
369
-
370
- `init --with` also writes `<directory>/package.json`: a private manifest that
371
- pins, at exactly the version that was resolved, the running runtime, every
372
- extension named in `--with`, and every package those extensions declare in
373
- `peerDependencies` (so `@jimhoyd/urlcode-ui` is pinned for an `auth,admin` site
374
- although nobody named it). Before anything is written, the whole set is checked
375
- against every declared peer range; an incompatible combination or a missing
376
- required peer refuses and names it, leaving no directory behind.
377
-
378
- Nothing is installed. The generated site has no `node_modules` and no
379
- `package-lock.json` until you run `npm install` in it yourself, which the
380
- command and the generated README both state as the next step. Reproducibility
381
- comes from that install, not from generation.
382
-
383
- - `--no-manifest` generates the site without a `package.json`, for a site whose
384
- dependencies are managed elsewhere. Plain `urlcode init` is unchanged and
385
- still writes no manifest; add `--manifest` to pin the runtime for a
386
- route-only project too.
387
- - `--pin <package>=<specifier>` records a specifier instead of the resolved
388
- version, for local tarball or offline development
389
- (`--pin @jimhoyd/urlcode-auth=file:/abs/urlcode-auth-0.1.0-alpha.6.tgz`). A
390
- package installed from a local path or tarball is detected from npm's own
391
- install record and pinned by that path without any flag; the README says so,
392
- because such a pin only reproduces where that path exists.
393
-
394
- There is no upgrade command. Moving a generated project to newer versions today
395
- means editing its `package.json` and re-running `npm install` yourself; nothing
396
- in this runtime selects a newer tested set, shows the change, or updates a
397
- lockfile for you.
398
-
399
- Serving the result is the usual explicit host binding:
400
-
401
- ```sh
402
- urlcode validate --project app --host-file "$PWD/host.mjs" --origin https://site.example
403
- ```
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) |