@jimhoyd/urlcode 0.3.0 → 0.4.0-alpha.2

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 (380) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +122 -0
  2. package/.claude/skills/urlcode-operations/SKILL.md +108 -0
  3. package/.claude-plugin/marketplace.json +18 -0
  4. package/CONTRIBUTING.md +30 -2
  5. package/README.md +195 -255
  6. package/ROADMAP.md +143 -15
  7. package/SECURITY.md +31 -9
  8. package/dist/BUILD-MANIFEST.json +72 -47
  9. package/dist/adapters.js +4 -23
  10. package/dist/agent-lists.js +1 -1
  11. package/dist/agents-guide.js +113 -0
  12. package/dist/authoring-files.js +60 -0
  13. package/dist/authoring.js +11 -1
  14. package/dist/aws.js +4 -3
  15. package/dist/build-cloudflare.js +11 -24
  16. package/dist/build-static.js +134 -0
  17. package/dist/bulk.js +37 -0
  18. package/dist/capabilities.js +262 -0
  19. package/dist/capability-query.js +71 -0
  20. package/dist/catalog.js +105 -0
  21. package/dist/cli.js +165 -34
  22. package/dist/client-address.js +1 -1
  23. package/dist/compliance-rules/baseline.js +9 -17
  24. package/dist/compliance-rules/privacy.js +7 -18
  25. package/dist/compliance-rules/shared.js +0 -2
  26. package/dist/compliance-rules/strict.js +5 -5
  27. package/dist/compliance.js +6 -8
  28. package/dist/conditions.js +88 -0
  29. package/dist/config.js +69 -6
  30. package/dist/context.js +155 -0
  31. package/dist/ecosystem-cli.js +88 -0
  32. package/dist/egress.js +98 -0
  33. package/dist/examples.js +92 -0
  34. package/dist/explain-cli.js +64 -0
  35. package/dist/explain.js +131 -0
  36. package/dist/extensions.js +231 -0
  37. package/dist/function-sources.js +49 -5
  38. package/dist/function-worker.js +3 -1
  39. package/dist/functions.js +84 -13
  40. package/dist/guest-api.js +29 -3
  41. package/dist/index.js +40 -6
  42. package/dist/init-with.js +165 -0
  43. package/dist/interchange-cli.js +42 -0
  44. package/dist/interchange.js +189 -0
  45. package/dist/manifest.js +109 -0
  46. package/dist/match.js +2 -2
  47. package/dist/mcp-authoring.js +147 -0
  48. package/dist/mcp.js +97 -0
  49. package/dist/observability.js +7 -21
  50. package/dist/operator-host.js +29 -0
  51. package/dist/plugins.js +12 -0
  52. package/dist/policies/agents.js +2 -2
  53. package/dist/policies/cache.js +8 -3
  54. package/dist/policies/compression.js +2 -1
  55. package/dist/policies/security.js +0 -0
  56. package/dist/policies.js +1 -1
  57. package/dist/policy.js +56 -15
  58. package/dist/prerender.js +100 -41
  59. package/dist/project-tests.js +3 -3
  60. package/dist/provider-verification.js +92 -0
  61. package/dist/proxy.js +44 -0
  62. package/dist/readiness.js +34 -11
  63. package/dist/recipes.js +41 -0
  64. package/dist/route-diff.js +106 -0
  65. package/dist/router.js +45 -7
  66. package/dist/runtime.js +164 -64
  67. package/dist/sandbox.js +48 -0
  68. package/dist/scaffold.js +0 -0
  69. package/dist/schema-query.js +62 -0
  70. package/dist/scripts/operational-drills.js +12 -54
  71. package/dist/server.js +3 -29
  72. package/dist/signals.js +24 -0
  73. package/dist/site.js +0 -0
  74. package/dist/tooling.js +96 -0
  75. package/dist/trusted-functions.js +210 -0
  76. package/dist/types/adapters.d.ts +7 -4
  77. package/dist/types/agent-lists.d.ts +0 -1
  78. package/dist/types/agents-guide.d.ts +17 -0
  79. package/dist/types/authoring-files.d.ts +10 -0
  80. package/dist/types/aws.d.ts +3 -1
  81. package/dist/types/build-cloudflare.d.ts +1 -0
  82. package/dist/types/build-static.d.ts +43 -0
  83. package/dist/types/bulk.d.ts +27 -0
  84. package/dist/types/capabilities.d.ts +64 -0
  85. package/dist/types/capability-query.d.ts +24 -0
  86. package/dist/types/catalog.d.ts +65 -0
  87. package/dist/types/client-address.d.ts +0 -1
  88. package/dist/types/compliance-rules/baseline.d.ts +1 -9
  89. package/dist/types/compliance-rules/privacy.d.ts +1 -4
  90. package/dist/types/compliance-rules/shared.d.ts +0 -2
  91. package/dist/types/compliance-rules/strict.d.ts +0 -5
  92. package/dist/types/compliance.d.ts +0 -3
  93. package/dist/types/conditions.d.ts +19 -0
  94. package/dist/types/config.d.ts +21 -2
  95. package/dist/types/context.d.ts +66 -0
  96. package/dist/types/ecosystem-cli.d.ts +17 -0
  97. package/dist/types/egress.d.ts +46 -0
  98. package/dist/types/examples.d.ts +50 -0
  99. package/dist/types/explain-cli.d.ts +11 -0
  100. package/dist/types/explain.d.ts +95 -0
  101. package/dist/types/extensions.d.ts +177 -0
  102. package/dist/types/function-sources.d.ts +9 -0
  103. package/dist/types/functions.d.ts +48 -5
  104. package/dist/types/guest-api.d.ts +1 -0
  105. package/dist/types/index.d.ts +36 -6
  106. package/dist/types/init-with.d.ts +30 -0
  107. package/dist/types/interchange-cli.d.ts +16 -0
  108. package/dist/types/interchange.d.ts +42 -0
  109. package/dist/types/manifest.d.ts +79 -0
  110. package/dist/types/match.d.ts +1 -0
  111. package/dist/types/mcp-authoring.d.ts +92 -0
  112. package/dist/types/mcp.d.ts +12 -0
  113. package/dist/types/observability.d.ts +3 -14
  114. package/dist/types/operator-host.d.ts +8 -0
  115. package/dist/types/plugins.d.ts +2 -0
  116. package/dist/types/policies/agents.d.ts +0 -2
  117. package/dist/types/policies/compression.d.ts +2 -0
  118. package/dist/types/policies/security.d.ts +0 -1
  119. package/dist/types/policy.d.ts +15 -4
  120. package/dist/types/project-tests.d.ts +3 -2
  121. package/dist/types/provider-verification.d.ts +53 -0
  122. package/dist/types/proxy.d.ts +21 -0
  123. package/dist/types/readiness.d.ts +10 -3
  124. package/dist/types/recipes.d.ts +30 -0
  125. package/dist/types/route-diff.d.ts +27 -0
  126. package/dist/types/router.d.ts +2 -1
  127. package/dist/types/runtime.d.ts +11 -27
  128. package/dist/types/sandbox.d.ts +12 -0
  129. package/dist/types/scaffold.d.ts +0 -2
  130. package/dist/types/schema-query.d.ts +12 -0
  131. package/dist/types/server.d.ts +1 -4
  132. package/dist/types/signals.d.ts +25 -0
  133. package/dist/types/site.d.ts +0 -1
  134. package/dist/types/tooling.d.ts +115 -0
  135. package/dist/types/trusted-functions.d.ts +29 -0
  136. package/dist/types/types.d.ts +71 -7
  137. package/dist/types/typescript-authoring.d.ts +12 -0
  138. package/dist/types/vercel.d.ts +3 -1
  139. package/dist/types/verify-deployment.d.ts +47 -0
  140. package/dist/types.js +37 -5
  141. package/dist/typescript-authoring.js +142 -0
  142. package/dist/vercel.js +4 -3
  143. package/dist/verify-deployment.js +270 -0
  144. package/docs/AI-AUTHORING.md +232 -15
  145. package/docs/AWS.md +4 -4
  146. package/docs/BEST-PRACTICES.md +3 -2
  147. package/docs/BULK.md +79 -0
  148. package/docs/CAPABILITIES.md +192 -0
  149. package/docs/CAPACITY.md +129 -32
  150. package/docs/CI.md +142 -0
  151. package/docs/CLOUDFLARE.md +1 -2
  152. package/docs/COMPLIANCE.md +6 -9
  153. package/docs/CONDITIONS.md +74 -0
  154. package/docs/DEPLOYMENT-CHECKS.md +108 -0
  155. package/docs/EGRESS.md +125 -0
  156. package/docs/EXTENSIONS.md +398 -0
  157. package/docs/FRAMEWORK.md +198 -0
  158. package/docs/FUNCTION-SECURITY.md +129 -32
  159. package/docs/INSTALL.md +45 -12
  160. package/docs/INTERCHANGE.md +134 -0
  161. package/docs/LOAD-TESTING.md +4 -4
  162. package/docs/MIDDLEWARE-EXAMPLES.md +75 -0
  163. package/docs/MIDDLEWARE.md +29 -16
  164. package/docs/MONITORING.md +2 -19
  165. package/docs/NEXT-PHASE-PLAN.md +98 -0
  166. package/docs/NEXT-STEPS.md +634 -0
  167. package/docs/OBSERVABILITY.md +11 -18
  168. package/docs/OPEN-DECISIONS.md +212 -0
  169. package/docs/OPERATIONAL-PROOF.md +30 -31
  170. package/docs/OPERATIONS.md +29 -35
  171. package/docs/PLUGINS.md +37 -0
  172. package/docs/POLICIES.md +23 -309
  173. package/docs/PRERENDER.md +41 -1
  174. package/docs/PROJECT-DIRECTION.md +75 -8
  175. package/docs/PROVIDER-VERIFICATION.md +84 -0
  176. package/docs/READINESS.md +21 -1
  177. package/docs/README.md +87 -34
  178. package/docs/RECIPES.md +99 -0
  179. package/docs/RELEASE-READINESS.md +57 -35
  180. package/docs/RELEASE-SECURITY.md +116 -7
  181. package/docs/RESILIENCE.md +16 -15
  182. package/docs/ROUTING.md +8 -10
  183. package/docs/SANDBOX-REVIEW.md +19 -6
  184. package/docs/SCAFFOLDING.md +0 -2
  185. package/docs/SECURITY-AUDIT.md +41 -1
  186. package/docs/SPECIFICATION.md +150 -29
  187. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +287 -0
  188. package/docs/SPIKE-BUSINESS-SUITE.md +1021 -0
  189. package/docs/SPIKE-CORE-LAYERING.md +337 -0
  190. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +209 -0
  191. package/docs/SPIKE-EXTENSION-MODEL.md +419 -0
  192. package/docs/SPIKE-EXTENSIONS.md +6 -0
  193. package/docs/SPIKE-LAMBDA-COMPILE.md +201 -0
  194. package/docs/SPIKE-MONOREPO.md +322 -0
  195. package/docs/STANDARDS.md +150 -142
  196. package/docs/STARTERS.md +21 -1
  197. package/docs/STATIC.md +94 -0
  198. package/docs/TOOLING.md +295 -0
  199. package/docs/TUNNELS.md +0 -3
  200. package/docs/TYPESCRIPT-AUTHORING.md +82 -0
  201. package/docs/TYPESCRIPT.md +25 -4
  202. package/docs/USABILITY-REVIEW.md +129 -0
  203. package/docs/VERCEL.md +4 -5
  204. package/docs/VERSION-ALIGNMENT.md +205 -0
  205. package/docs/YAML-GUIDE.md +15 -479
  206. package/docs/YAML-REFERENCE.md +143 -22
  207. package/docs/policies/agents.md +1 -1
  208. package/docs/policies/cache.md +13 -0
  209. package/docs/policies/contract.md +52 -0
  210. package/docs/policies/hardened.md +56 -0
  211. package/docs/policies/interoperability.md +169 -0
  212. package/docs/policies/operations.md +45 -0
  213. package/docs/yaml/assets.md +36 -0
  214. package/docs/yaml/conditions.md +20 -0
  215. package/docs/yaml/functions.md +160 -0
  216. package/docs/yaml/middleware.md +29 -0
  217. package/docs/yaml/organization.md +74 -0
  218. package/docs/yaml/policies.md +37 -0
  219. package/docs/yaml/redirects.md +64 -0
  220. package/docs/yaml/responses.md +57 -0
  221. package/docs/yaml/site.md +24 -0
  222. package/examples/assets/example.yaml +17 -0
  223. package/examples/aws/example.yaml +20 -0
  224. package/examples/cloudflare/example.yaml +19 -0
  225. package/examples/compliance/example.yaml +11 -0
  226. package/examples/conditions/README.md +12 -0
  227. package/examples/conditions/example.yaml +19 -0
  228. package/examples/conditions/tests/requests.json +13 -0
  229. package/examples/conditions/urlcode.yaml +24 -0
  230. package/examples/cookbook/README.md +8 -4
  231. package/examples/cookbook/example.yaml +17 -0
  232. package/examples/cookbook/functions/catalog.mjs +3 -0
  233. package/examples/cookbook/functions/fail.mjs +4 -0
  234. package/examples/cookbook/functions/items.mjs +3 -0
  235. package/examples/cookbook/functions/profile.mjs +3 -0
  236. package/examples/cookbook/functions/resource.mjs +3 -0
  237. package/examples/cookbook/functions/status.mjs +3 -0
  238. package/examples/cookbook/middleware/auth.mjs +48 -0
  239. package/examples/cookbook/middleware/body.mjs +15 -0
  240. package/examples/cookbook/middleware/bucket.mjs +29 -0
  241. package/examples/cookbook/middleware/cors.mjs +21 -0
  242. package/examples/cookbook/middleware/debug.mjs +13 -0
  243. package/examples/cookbook/middleware/envelope.mjs +11 -0
  244. package/examples/cookbook/middleware/errors.mjs +11 -0
  245. package/examples/cookbook/middleware/etag.mjs +18 -0
  246. package/examples/cookbook/middleware/locale.mjs +20 -0
  247. package/examples/cookbook/middleware/maintenance.mjs +10 -0
  248. package/examples/cookbook/middleware/methods.mjs +15 -0
  249. package/examples/cookbook/middleware/negotiate.mjs +20 -0
  250. package/examples/cookbook/middleware/referer.mjs +12 -0
  251. package/examples/cookbook/middleware/request-id.mjs +16 -0
  252. package/examples/cookbook/route-index.json +676 -0
  253. package/examples/cookbook/routes/middleware.yaml +126 -0
  254. package/examples/cookbook/tests/requests.json +526 -0
  255. package/examples/cookbook/urlcode.yaml +1 -0
  256. package/examples/egress/README.md +22 -0
  257. package/examples/egress/example.yaml +19 -0
  258. package/examples/egress/urlcode.yaml +19 -0
  259. package/examples/extensions/README.md +7 -0
  260. package/examples/extensions/example.yaml +21 -0
  261. package/examples/extensions/urlcode.yaml +25 -0
  262. package/examples/monitoring/example.yaml +8 -0
  263. package/examples/prerender/README.md +2 -2
  264. package/examples/prerender/example.yaml +16 -0
  265. package/examples/provider-conformance/README.md +12 -0
  266. package/examples/provider-conformance/example.yaml +14 -0
  267. package/examples/provider-conformance/urlcode.yaml +34 -0
  268. package/examples/tunnel/example.yaml +8 -0
  269. package/examples/vercel/example.yaml +19 -0
  270. package/llms-full.txt +3084 -0
  271. package/llms.txt +61 -21
  272. package/package.json +36 -7
  273. package/packaging/claude-plugin/.claude-plugin/plugin.json +19 -0
  274. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +122 -0
  275. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +108 -0
  276. package/recipes/authenticated-json-api/README.md +51 -0
  277. package/recipes/authenticated-json-api/functions/profile.mjs +5 -0
  278. package/recipes/authenticated-json-api/recipe.yaml +34 -0
  279. package/recipes/authenticated-json-api/tests/requests.json +39 -0
  280. package/recipes/authenticated-json-api/urlcode.yaml +12 -0
  281. package/recipes/contact-form/README.md +25 -0
  282. package/recipes/contact-form/functions/contact.mjs +17 -0
  283. package/recipes/contact-form/recipe.yaml +33 -0
  284. package/recipes/contact-form/tests/requests.json +47 -0
  285. package/recipes/contact-form/urlcode.yaml +18 -0
  286. package/recipes/cors-api/README.md +16 -0
  287. package/recipes/cors-api/functions/items.mjs +3 -0
  288. package/recipes/cors-api/middleware/cors.mjs +21 -0
  289. package/recipes/cors-api/recipe.yaml +26 -0
  290. package/recipes/cors-api/tests/requests.json +65 -0
  291. package/recipes/cors-api/urlcode.yaml +12 -0
  292. package/recipes/health-page/README.md +13 -0
  293. package/recipes/health-page/recipe.yaml +23 -0
  294. package/recipes/health-page/tests/requests.json +36 -0
  295. package/recipes/health-page/urlcode.yaml +19 -0
  296. package/recipes/json-api/README.md +6 -0
  297. package/recipes/json-api/functions/echo.mjs +3 -0
  298. package/recipes/json-api/recipe.yaml +25 -0
  299. package/recipes/json-api/tests/requests.json +34 -0
  300. package/recipes/json-api/urlcode.yaml +12 -0
  301. package/recipes/middleware/README.md +34 -0
  302. package/recipes/middleware/functions/catalog.mjs +3 -0
  303. package/recipes/middleware/functions/fail.mjs +4 -0
  304. package/recipes/middleware/functions/items.mjs +3 -0
  305. package/recipes/middleware/functions/profile.mjs +3 -0
  306. package/recipes/middleware/functions/resource.mjs +3 -0
  307. package/recipes/middleware/functions/status.mjs +3 -0
  308. package/recipes/middleware/middleware/auth.mjs +48 -0
  309. package/recipes/middleware/middleware/body.mjs +15 -0
  310. package/recipes/middleware/middleware/bucket.mjs +29 -0
  311. package/recipes/middleware/middleware/cors.mjs +21 -0
  312. package/recipes/middleware/middleware/debug.mjs +13 -0
  313. package/recipes/middleware/middleware/envelope.mjs +11 -0
  314. package/recipes/middleware/middleware/errors.mjs +11 -0
  315. package/recipes/middleware/middleware/etag.mjs +18 -0
  316. package/recipes/middleware/middleware/locale.mjs +20 -0
  317. package/recipes/middleware/middleware/maintenance.mjs +10 -0
  318. package/recipes/middleware/middleware/methods.mjs +15 -0
  319. package/recipes/middleware/middleware/negotiate.mjs +20 -0
  320. package/recipes/middleware/middleware/referer.mjs +12 -0
  321. package/recipes/middleware/middleware/request-id.mjs +16 -0
  322. package/recipes/middleware/public/guide.txt +1 -0
  323. package/recipes/middleware/recipe.yaml +50 -0
  324. package/recipes/middleware/tests/requests.json +528 -0
  325. package/recipes/middleware/urlcode.yaml +127 -0
  326. package/recipes/protected-download/README.md +22 -0
  327. package/recipes/protected-download/files/report.txt +1 -0
  328. package/recipes/protected-download/recipe.yaml +31 -0
  329. package/recipes/protected-download/tests/requests.json +32 -0
  330. package/recipes/protected-download/urlcode.yaml +15 -0
  331. package/recipes/redirect/README.md +7 -0
  332. package/recipes/redirect/recipe.yaml +25 -0
  333. package/recipes/redirect/tests/requests.json +19 -0
  334. package/recipes/redirect/urlcode.yaml +9 -0
  335. package/recipes/static-plus-api/README.md +15 -0
  336. package/recipes/static-plus-api/functions/info.mjs +3 -0
  337. package/recipes/static-plus-api/public/assets/index.html +3 -0
  338. package/recipes/static-plus-api/public/assets/site.css +1 -0
  339. package/recipes/static-plus-api/public/index.html +8 -0
  340. package/recipes/static-plus-api/recipe.yaml +29 -0
  341. package/recipes/static-plus-api/tests/requests.json +56 -0
  342. package/recipes/static-plus-api/urlcode.yaml +23 -0
  343. package/recipes/typescript/README.md +8 -0
  344. package/recipes/typescript/functions/hello.ts +5 -0
  345. package/recipes/typescript/recipe.yaml +23 -0
  346. package/recipes/typescript/tests/requests.json +18 -0
  347. package/recipes/typescript/urlcode.yaml +5 -0
  348. package/recipes/webhook-receiver/README.md +20 -0
  349. package/recipes/webhook-receiver/functions/receive.mjs +16 -0
  350. package/recipes/webhook-receiver/recipe.yaml +27 -0
  351. package/recipes/webhook-receiver/tests/requests.json +59 -0
  352. package/recipes/webhook-receiver/urlcode.yaml +23 -0
  353. package/schemas/recipe.schema.json +139 -0
  354. package/schemas/urlcode.schema.json +659 -110
  355. package/skills/urlcode/SKILL.md +119 -0
  356. package/starters/default/.github/workflows/urlcode.yml +23 -0
  357. package/starters/default/.mcp.json +12 -0
  358. package/starters/default/AGENTS.md +79 -0
  359. package/starters/default/urlcode.yaml +0 -1
  360. package/dist/link-api.js +0 -136
  361. package/dist/link-cli.js +0 -141
  362. package/dist/link-events.js +0 -76
  363. package/dist/link-records.js +0 -31
  364. package/dist/link-store-worker.js +0 -150
  365. package/dist/link-store.js +0 -250
  366. package/dist/management-policy.js +0 -41
  367. package/dist/sqlite-version.js +0 -6
  368. package/dist/types/link-api.d.ts +0 -30
  369. package/dist/types/link-cli.d.ts +0 -36
  370. package/dist/types/link-events.d.ts +0 -27
  371. package/dist/types/link-records.d.ts +0 -11
  372. package/dist/types/link-store-worker.d.ts +0 -1
  373. package/dist/types/link-store.d.ts +0 -130
  374. package/dist/types/management-policy.d.ts +0 -9
  375. package/dist/types/sqlite-version.d.ts +0 -1
  376. package/docs/DYNAMIC-LINKS.md +0 -561
  377. package/docs/MANAGEMENT-SECURITY.md +0 -82
  378. package/examples/live-links/README.md +0 -11
  379. package/examples/live-links/tests/requests.json +0 -6
  380. package/examples/live-links/urlcode.yaml +0 -16
@@ -0,0 +1,398 @@
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 are moving out of core this way too: a future
10
+ `urlcode-dynamic-link` package (mount-based, like `auth`/`admin`, not yet
11
+ published) will own the durable link store, its CLI and management API. Core
12
+ no longer has a native `link` handler or a `dynamicLinks` project flag.
13
+
14
+ A project declares versioned configuration and exclusive route mounts:
15
+
16
+ ```yaml
17
+ version: "1"
18
+ extensions:
19
+ auth:
20
+ version: "1"
21
+ config: {}
22
+ routes:
23
+ /auth/*:
24
+ extension: auth
25
+ methods: [GET, HEAD, POST]
26
+ /private:
27
+ respond: {text: Private}
28
+ policies:
29
+ extensions:
30
+ auth: {signedIn: true}
31
+ ```
32
+
33
+ ## Protecting a route: the `auth` short form
34
+
35
+ When the project declares `extensions.auth`, a route may say `auth` instead of
36
+ spelling out `policies.extensions.auth`. This is the preferred way to protect a
37
+ route:
38
+
39
+ ```yaml
40
+ routes:
41
+ /account:
42
+ respond: {text: Account}
43
+ auth: {role: member} # or `auth: true` for any signed-in principal
44
+ /docs:
45
+ respond: {text: Docs}
46
+ auth: {required: false} # documents intent; emits no requirement
47
+ ```
48
+
49
+ The compiler expands the short form before anything else reads the project:
50
+ `auth: true` becomes `policies.extensions.auth: {}` and an object becomes the
51
+ same object minus `required`. The long form stays the canonical representation,
52
+ so `routes`, `audit` and `explain` show the expansion, the extension revision
53
+ hash covers it, and the installed auth extension validates the expanded
54
+ requirement with its own policy schema. The keys other than `required` are
55
+ exactly that schema's keys (`role`, `permission`, `verified`,
56
+ `freshWithinSeconds`, `onDeny`); the runtime adds nothing of its own. Loading
57
+ fails, naming the route, when `auth` appears without an `extensions.auth`
58
+ declaration, next to `policies.extensions.auth`, or next to
59
+ `policies.extensions: false`.
60
+
61
+ The same shape is used for the cache policy: a route-level `cache: {strategy,
62
+ maxAge, ...}` expands to `policies.cache` in the same pass (see
63
+ [policies](POLICIES.md)).
64
+
65
+ The configuration and requirement objects above are validated by the installed
66
+ extension's schemas. They are examples of extension-owned fields, not built-in
67
+ authentication behavior. See the executable generic fixture in
68
+ [examples/extensions](../examples/extensions). Included files can declare
69
+ extensions; duplicate names fail rather than silently override one another.
70
+
71
+ The operator passes `extensions: RuntimeExtension[]` to `createRuntime`,
72
+ `startServer`, or the AWS/Vercel adapter. Types and
73
+ `inspectExtensionRevision(project)` are exported from
74
+ `@jimhoyd/urlcode/extensions`. Inspection does not grant access: review the
75
+ project and place the exact returned SHA-256 in each registration's
76
+ `projectSha256`. YAML extension configuration, policies and routes participate
77
+ in the revision. Changing them requires an explicit operator reapproval.
78
+
79
+ Registrations provide a name, contract version, target list, JSON configuration
80
+ schema, optional policy schema, an optional declared `cacheSensitive` (below)
81
+ and activation factory. Activation receives the
82
+ canonical operator origin, target, revision and mount bases. Its instance handles
83
+ bounded requests and, when named in a route's policies, gates the request via
84
+ `authorize`, wraps the rest of the pipeline via `middleware`, or both (see
85
+ [Wrapping a route](#wrapping-a-route-extension-middleware) above). Missing
86
+ registrations, stale grants, invalid configuration and unsupported targets fail
87
+ activation. Multiple mounts cannot overlap other declared routes.
88
+
89
+ For extension-protected routes, agents/throttle run before authorization and
90
+ cache access happens only after authorization. This part is unconditional:
91
+ naming any extension in `policies.extensions` always runs its `authorize()`
92
+ (when it implements one) before the route's own handler, whatever this
93
+ section says next.
94
+
95
+ An `extension:` mount is always confidential: its route rejects cache
96
+ strategies other than no-store, and every response is forced to no-store
97
+ after host response hooks, with compression disabled. A `policies.extensions`
98
+ route (no mount, `authorize`/`middleware` only) gets the same treatment
99
+ **unless every extension it names explicitly declares
100
+ `cacheSensitive: false`** on its `RuntimeExtension` registration. That field
101
+ defaults to sensitive (unset or `true`): the safe default is unchanged, and
102
+ relaxing it is an explicit, reviewed operator opt-in an extension author
103
+ makes once, in host code, never inferred from a route or from a response the
104
+ extension happens to return. It exists for a generic, cache-transparent
105
+ extension whose `middleware()` is pure request/response wrapping with no
106
+ access-control semantics of its own (a logging or header-rewriting
107
+ extension, for example) — declared this way, its wrapped route keeps
108
+ whatever `Cache-Control` its own handler sets, exactly like the native
109
+ `middleware:` array already does, and compression is not disabled either. A
110
+ route naming more than one extension stays confidential if any one of them
111
+ is sensitive (or leaves the field unset); one `cacheSensitive: false`
112
+ extension cannot relax a route that also names a sensitive one. This can
113
+ only relax the no-store floor a generic extension would otherwise inherit —
114
+ it has no effect on `authorize()`, which runs the same way regardless, and
115
+ `auth`/`admin`-style extensions gating real access must leave it at the
116
+ default.
117
+
118
+ ## Wrapping a route: extension middleware
119
+
120
+ `authorize` is a gate: it runs once, before the route's handler, and can only
121
+ either let the request through unchanged or answer instead of it. It cannot
122
+ see or change what the handler itself returns.
123
+
124
+ `middleware` is a wrap. An extension instance may implement it alongside or
125
+ instead of `authorize`, attached the same way, via
126
+ `policies.extensions.<name>` on a route (no `extension:` mount required); its
127
+ `config` is exactly the same per-route value `authorize`'s `requirement`
128
+ receives, validated once against the extension's `policySchema`:
129
+
130
+ ```ts
131
+ middleware?(config: Readonly<Record<string, unknown>>, request: ExtensionRequest,
132
+ next: () => Promise<HandlerResult>): HandlerResult | Promise<HandlerResult>;
133
+ ```
134
+
135
+ `next()` invokes the rest of the pipeline for that route: any other extension
136
+ `middleware()` also declared on the route (see below), then the route's own
137
+ native `middleware:` chain and handler, dispatched through the sandboxed or
138
+ trusted engine exactly as it is today. Calling it lets the hook run code
139
+ before and after the rest of the pipeline, inspecting or mutating the
140
+ `HandlerResult` it resolves to — the same "add a header to whatever the
141
+ handler returns" shape as the native `middleware/headers.mjs` cookbook
142
+ recipe, but declared by an operator-installed extension instead of project
143
+ code. Skipping it short-circuits everything after that point, the same
144
+ capability `authorize` already has, just usable from either side of the
145
+ handler now. `next()` may be called at most once; calling it again throws.
146
+
147
+ A route naming more than one extension in `policies.extensions` chains every
148
+ one that implements `middleware`, in the order the keys are declared, each
149
+ one's `next()` reaching the next one and the innermost `next()` reaching the
150
+ native pipeline — the first declared name is outermost. This is purely
151
+ additive at the `policies.extensions` layer and never touches the native
152
+ `middleware:` array, its schema, or its dispatch, all of which are unchanged.
153
+
154
+ `authorize` and `middleware` compose on the same route, from the same or
155
+ different extensions, without special-casing: `authorize` always runs first
156
+ (unchanged), and any declared `middleware()` wraps everything after that
157
+ point, including the rest of the authorize-gated pipeline. A route naming an
158
+ extension via `policies.extensions` only requires that extension to
159
+ implement `authorize`, `middleware`, or both — never both unconditionally.
160
+
161
+ One exception exists for content-hashed assets. A registration may declare
162
+ `immutableAssets: {prefix: '/static'}`, a normalized literal path under each
163
+ of its mounts (no `.` or `..` segments, no trailing slash). The runtime then
164
+ answers `Cache-Control: public, max-age=31536000, immutable` instead of
165
+ no-store only when every condition holds: the request path lies under
166
+ `<mount><prefix>/`, the method is GET or HEAD, the status is 200 or 304, the
167
+ response carries exactly one strong ETag, sets no Set-Cookie, and does not
168
+ vary on Cookie, Authorization or `*`. A stricter Cache-Control the extension
169
+ set (no-store, no-cache, private or a shorter max-age) is preserved; other CDN
170
+ cache headers are still stripped and compression stays disabled. Anything
171
+ that fails a condition, including a cookie added by a later response hook,
172
+ stays no-store. The extension owns the content-hashed filename: a file under
173
+ the prefix must change its name when its bytes change, because clients never
174
+ revalidate it. The prefix belongs to the operator registration, not to the
175
+ pinned project revision. The runtime withholds Cookie and Authorization plus any declared
176
+ credential headers from all application guest requests and mapped parameters.
177
+ This does not isolate browser JavaScript running on the same origin: application
178
+ HTML/JS on an authentication origin must be trusted by that site's operator.
179
+
180
+ Cloudflare refuses extensions until its artifact format supports their execution.
181
+ Node adapter conformance is not a live-provider deployment claim.
182
+
183
+ ## Project-level lifecycle hooks
184
+
185
+ `authorize` and `middleware` let core call *into* an extension. They do not
186
+ let a project hand its own code *to* an extension to run at a defined point.
187
+ That gap matters once an extension has meaningful lifecycle events —
188
+ registration, deletion, an administrative action, a link resolution, and so
189
+ on. Presentation already has a standard layering mechanism for this: a
190
+ project customizes an extension's *look* through `urlcode-ui`'s
191
+ `copy`/`extra.css`/`templates` without forking it. Behavior needs the same
192
+ standard, or every extension author (and every project depending on one)
193
+ either reinvents it or forks the extension. `urlcode-auth` and
194
+ `urlcode-admin` independently hit this gap (auth/#35, admin/#32); auth's own
195
+ [SPIKE-AUTH.md](https://github.com/jimhoyd-com/urlcode-auth/blob/main/docs/SPIKE-AUTH.md)
196
+ already scoped a shape for `onSignUp`/`beforeRegister`/`onDelete`.
197
+
198
+ **The pattern.** An extension with lifecycle events an author judges worth
199
+ exposing should let the project name its own function in the extension's own
200
+ `config`, using the same source shape `function`/`middleware` routes already
201
+ use (a string path, or `{source, export, args}` — `schemas/urlcode.schema.json`),
202
+ and add its own `sandbox` boolean next to it (below). The extension's own
203
+ `activate()` reads that config, and its own runtime dispatch — not a new core
204
+ primitive, an ordinary call the extension package makes with the request
205
+ context it already has — invokes the named function at the lifecycle point
206
+ it defines, with a typed input and a typed verdict the extension's own
207
+ schema documents. For example, an auth-style extension might declare:
208
+
209
+ ```yaml
210
+ extensions:
211
+ auth:
212
+ version: "1"
213
+ config:
214
+ hooks:
215
+ beforeRegister:
216
+ source: ./hooks/registration-rule.mjs
217
+ export: default
218
+ onSignUp:
219
+ source: ./hooks/on-signup.mjs
220
+ ```
221
+
222
+ with `beforeRegister` called before an account is created, given a typed
223
+ `{email, traits}`-shaped input and returning a typed verdict (`{allow: true}`
224
+ or `{allow: false, reason}`), and `onSignUp` called after, for side effects
225
+ such as provisioning a workspace. The exact hook names, input/verdict shapes
226
+ and invocation points are the extension's own design — `hooks` is not a core
227
+ schema key — but the source/export/sandbox shape, and the fact that this is
228
+ project code the extension calls rather than an operator callback in
229
+ `host.mjs`, should be consistent across extensions so an author who has
230
+ learned one has learned the pattern.
231
+
232
+ **Trust and execution mode.** Project-level lifecycle hooks are first-party
233
+ project code, the same category as any `function`/`middleware` route, and
234
+ follow the same trusted-by-default rule with no special case
235
+ (docs/SPIKE-DEFAULT-TRUST-MODEL.md, [FUNCTION-SECURITY.md](FUNCTION-SECURITY.md)):
236
+ trusted, in-process execution by default; a project sets `sandbox: true` on
237
+ a given hook to opt that hook into isolated QuickJS/WASM execution, exactly
238
+ the mechanism `function`/`middleware` routes already use and for the same
239
+ reason — the hook's own code, not the trustworthiness of whatever triggered
240
+ it, is what calls for isolation (docs/AI-AUTHORING.md's "Deciding when a
241
+ route needs `sandbox: true`"). This was raised as an open question — whether
242
+ a lifecycle hook should always run sandboxed because it makes a
243
+ security-relevant decision — and settled the other way: uniformity with the
244
+ rest of the trust model was chosen over hardwiring isolation for lifecycle
245
+ hooks specifically, the same "no special case" call already made between
246
+ `function` and `middleware` trust. A `beforeRegister` hook enforcing "only
247
+ `@acme.com` may register" is the project's own governance rule over its own
248
+ signup flow; it is not more dangerous than any other route the project
249
+ wrote, and does not get a different default.
250
+
251
+ Core's own trusted/sandboxed dispatch (`TrustedFunctions`/`FunctionPool`,
252
+ `src/runtime.ts`) is wired to route dispatch, not exposed to extensions — but
253
+ each half of a hook's `sandbox: true` opt-in has its own answer:
254
+
255
+ - **Trusted (the default, no `sandbox: true`).** No core primitive is needed
256
+ or provided: this is ordinary first-party project code, and the
257
+ extension's own `activate()` already has `ExtensionActivation.root` to
258
+ resolve the hook's `source` against and can `import()` it directly, the
259
+ same way any trusted `function`/`middleware` route does.
260
+ - **Sandboxed (`sandbox: true`).** `@jimhoyd/urlcode/sandbox` exports
261
+ `SandboxPool`, the same QuickJS/worker-thread engine that backs a
262
+ sandboxed `function`/`middleware` route — the identical module-allowlist
263
+ walk, memory/stack limits, two-layer deadline enforcement, `maxBytes` and
264
+ response-shape validation, with no separate or weaker engine for
265
+ extensions. It takes an explicit list of `{source, export}` entries
266
+ (resolve a hook's `source` string with the re-exported `functionFile()`,
267
+ the same resolution/validation a native route's `source` gets) instead of
268
+ anything route/YAML-shaped, and `execute({entry, chain}, request, context)`
269
+ in place of a `FunctionRoute`. There is no "trusted" mode exported
270
+ alongside it — `SandboxPool` is only ever the isolated path; see
271
+ [FUNCTION-SECURITY.md](FUNCTION-SECURITY.md) and
272
+ [TYPESCRIPT.md](TYPESCRIPT.md) for the full contract.
273
+
274
+ An extension honoring a hook's `sandbox: true` is expected to actually
275
+ isolate that invocation through `SandboxPool` now that the primitive exists
276
+ (or document plainly that it does not yet, rather than accepting the field
277
+ and silently running it trusted) — say which, in the extension's own docs,
278
+ so an author reading them is not misled about what opt-in exists.
279
+
280
+ ## Discovering schemas
281
+
282
+ Each registration carries the JSON Schemas that validate its `config` block and
283
+ its per-route policy requirements. `urlcode extensions` prints them together with
284
+ the project's own declarations so an author can see what a mount accepts:
285
+
286
+ ```sh
287
+ urlcode extensions --project ./site --host-file /absolute/operator/host.mjs [--json]
288
+ ```
289
+
290
+ For every registration in the host file it reports the name, contract version,
291
+ targets, credential headers, configuration schema, policy schema (if any),
292
+ whether the project declares it, whether its `projectSha256` matches the current
293
+ revision, the routes that mount it and the routes whose policies require it.
294
+ Declared names the host does not register are listed as unregistered. The command
295
+ executes the trusted host module exactly as `validate` does, including its
296
+ absolute-path and outside-project rules, and calls `close` afterwards; it never
297
+ activates an extension and grants nothing. Without `--host-file` it lists only
298
+ the names the project declares and notes that schemas need the host file.
299
+
300
+ The same report is available as `inspectExtensions({project, hostFile?})` from
301
+ the package root and, for assistants, as the MCP tool `get_extensions`, which the
302
+ server advertises only when the operator started `urlcode mcp` with
303
+ `--host-file`. No tool argument can name a host file. See [TOOLING.md](TOOLING.md).
304
+
305
+ ## CLI host binding
306
+
307
+ Use an explicitly named operator ES module outside the application directory:
308
+
309
+ ```sh
310
+ urlcode serve --project ./site --origin https://site.example \
311
+ --host-file /absolute/operator/host.mjs
312
+ ```
313
+
314
+ The module default-exports `{extensions, plugins?, close?}`. It may import installed
315
+ operator packages, open their stores and read operator secrets. `close` releases
316
+ shared services when the CLI command finishes or the server shuts down. A runtime
317
+ reload closes extension instances but does not close caller-owned services. Host
318
+ modules are not watched or automatically rediscovered. Restart to update them.
319
+
320
+ The same explicit option is supported by dev, validate, test, routes, audit,
321
+ benchmark, extensions and mcp. These commands execute trusted host activation and may access its
322
+ store; read-only project inspection commands never implicitly load a host file.
323
+ Host-file paths must be absolute `.mjs`/`.js` files whose real path lies outside
324
+ the project, including after symlink resolution. This is an operator-code trust
325
+ boundary, not a JavaScript sandbox or an independent security review.
326
+
327
+ ## Scaffolding with `init --with`
328
+
329
+ `urlcode init <directory> --with auth,admin` produces the layered site the
330
+ [framework page](FRAMEWORK.md#the-composition-contract) describes in one
331
+ command: the starter under `<directory>/app/`, one `host.mjs`, one `README.md`,
332
+ and each extension's own operator files. Core never bundles or imports the
333
+ extension packages at build time; at run time it resolves
334
+ `@jimhoyd/urlcode-<name>` for each name with Node's package resolution from
335
+ the invoking directory (so `npm install @jimhoyd/urlcode-auth` in that
336
+ directory, from npm where the packages are published as `0.1.0-alpha.x`
337
+ prereleases, is the normal path and what makes `--with auth` work), imports
338
+ the package and calls its
339
+ `scaffold` export with this request:
340
+
341
+ ```ts
342
+ interface ScaffoldRequest {
343
+ directory: string; // absolute site directory; result file paths are relative to it
344
+ project: string; // absolute route project, <directory>/app (holds urlcode.yaml)
345
+ hostFile: string; // absolute combined host module, <directory>/host.mjs
346
+ names: readonly string[]; // every name in --with order, including this one
347
+ }
348
+ interface ScaffoldFile { path: string; content: string | Uint8Array; mode?: number }
349
+ interface ScaffoldResult {
350
+ name: string; // must equal the requested name
351
+ extensions: Record<string, unknown>; // merged into the project's top-level extensions
352
+ routes: Record<string, unknown>; // merged into app/routes/extensions.yaml
353
+ hostImports: string[]; hostSetup: string[]; hostEntries: string[]; hostClose?: string[];
354
+ files: ScaffoldFile[]; // written relative to directory with their modes
355
+ readme: string; nextSteps: string[]; // README section and numbered steps
356
+ env?: Record<string, string>; // environment variables the host reads
357
+ }
358
+ ```
359
+
360
+ `scaffold` writes nothing; it returns fragments and may generate key material
361
+ in memory (core zeroes `Uint8Array` contents after writing or on failure). The
362
+ types are exported from `@jimhoyd/urlcode` for packages that want to typecheck
363
+ against them.
364
+
365
+ Assembly rules, in `--with` order:
366
+
367
+ - Every package is resolved and every `scaffold` is called before anything is
368
+ written. A name that is not installed refuses with the `npm install` command;
369
+ a package without a `scaffold` export refuses and names the package; an error
370
+ thrown by a `scaffold` (for example admin without auth in the same `--with`)
371
+ is reported as that package's refusal. No directory is left behind.
372
+ - `extensions` fragments are declared in `app/urlcode.yaml`; `routes`
373
+ fragments are written to `app/routes/extensions.yaml`, appended to the
374
+ starter's `includes`, so the starter's own routes load first. A route or
375
+ extension key produced twice, or one the starter already declares, is refused
376
+ naming both sources.
377
+ - `host.mjs` is all `hostImports`, then all `hostSetup` lines, then an
378
+ `extensions` array of every `hostEntries` item, then `close()` running the
379
+ `hostClose` statements in reverse `--with` order so later entries release
380
+ before what they built on. Setup lines share one module scope: admin's entry
381
+ references the `service`, `csrfKey` and `projectSha256` identifiers that
382
+ auth's setup defines, which is why `names` carries the full list.
383
+ - `files` are created exclusively (`wx`) with their `mode` (default `0644`),
384
+ must stay inside the site directory and outside `app/`, and never pass
385
+ through a symlink. Nothing generated is ever overwritten; an existing
386
+ destination refuses like plain `init`.
387
+ - `README.md` holds the starter's README as a section, then each result's
388
+ `readme` under `## Extension: <name>`, the merged numbered `nextSteps`, the
389
+ merged `env` table and the project revision. The command prints that
390
+ revision (`inspectExtensionRevision` of `app/`) with the instruction to
391
+ review the project and pin it explicitly; the host is generated to require
392
+ the pin, never to compute it.
393
+
394
+ Serving the result is the usual explicit host binding:
395
+
396
+ ```sh
397
+ urlcode validate --project app --host-file "$PWD/host.mjs" --origin https://site.example
398
+ ```
@@ -0,0 +1,198 @@
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 | Repository | What it adds | How a project declares it |
11
+ |---|---|---|---|
12
+ | `@jimhoyd/urlcode` | [urlcode](https://github.com/jimhoyd-com/urlcode) | The runtime: YAML routes, functions and middleware (trusted by default, `sandbox: true` opt-in), pages and assets, policies, site conventions, CLI, provider adapters, the extension contract | `urlcode.yaml` with `version: "1"` |
13
+ | `@jimhoyd/urlcode-ui` | [urlcode-ui](https://github.com/jimhoyd-com/urlcode-ui) | Shared presentation: escaped templates, shadcn/ui partials, one stylesheet with light and dark, themes, translations, the `ui` extension that serves the kit's assets | `extensions.ui` plus an asset mount route |
14
+ | `@jimhoyd/urlcode-auth` | [urlcode-auth](https://github.com/jimhoyd-com/urlcode-auth) | Accounts: password, passkeys, OpenID Connect, email codes, TOTP, recovery, sessions, roles, registration modes, account page, operator CLI | `extensions.auth` plus an `/account/*` mount and `policies.extensions.auth` on protected routes |
15
+ | `@jimhoyd/urlcode-admin` | [urlcode-admin](https://github.com/jimhoyd-com/urlcode-admin) | Administration: users, sessions, roles, audit, registration approval, two-person cases, support impersonation, health | `extensions.admin` plus an `/admin/*` mount |
16
+
17
+ The core is Apache-2.0 and released. The three extension packages are
18
+ Apache-2.0 and published to npm as alphas (`@jimhoyd/urlcode-ui@0.1.0-alpha.4`,
19
+ `@jimhoyd/urlcode-auth@0.1.0-alpha.2`, `@jimhoyd/urlcode-admin@0.1.0-alpha.2`,
20
+ on core `0.4.0-alpha.1`). An alpha on npm is a distribution channel, not an
21
+ endorsement: the source is complete, but independent review, deployment
22
+ evidence and an accessibility assessment are still pending
23
+ ([issue 58](https://github.com/jimhoyd-com/urlcode/issues/58)). Their status
24
+ files say exactly what is built: [auth](https://github.com/jimhoyd-com/urlcode-auth/blob/main/IMPLEMENTATION-STATUS.md),
25
+ [admin](https://github.com/jimhoyd-com/urlcode-admin/blob/main/IMPLEMENTATION-STATUS.md),
26
+ [ui](https://github.com/jimhoyd-com/urlcode-ui/blob/main/IMPLEMENTATION-STATUS.md).
27
+ Which core version each package supports, how it declares that, and the order
28
+ in which a core change reaches the downstream repositories are recorded in
29
+ [core version alignment](VERSION-ALIGNMENT.md).
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). Secrets reach a function
47
+ only through an operator grant pinned to the project revision.
48
+ 4. **Accounts.** The `auth` extension: sign-in, registration, MFA, account
49
+ page and protected routes. The operator installs it in a host file outside
50
+ the project; YAML only declares the mount and configuration.
51
+ 5. **Administration.** The `admin` extension on the same service: manage the
52
+ people who signed up, their sessions and roles, review the audit trail.
53
+
54
+ 6. **Your own look.** A shared `presentation` (catalogue and theme variables)
55
+ restyles auth and admin together; the `ui` extension adds the template kit,
56
+ project copy, template and stylesheet overrides for kit-rendered pages.
57
+
58
+ Stored short links previously sat here as a native `link` route; that handler
59
+ was removed from core. A future `urlcode-dynamic-link` package (not yet
60
+ published, so this one line is the exception to "nothing is roadmap" above)
61
+ is expected to own them the same way `auth`/`admin` own their mounts.
62
+
63
+ Rungs 1 to 3 need only the core package. Rungs 4 to 6 need the extension
64
+ packages, installed from npm as `0.1.0-alpha.x` prereleases, and a Node host
65
+ with a patched SQLite build; see each repository's README for the exact
66
+ requirement.
67
+
68
+ ## The composition contract
69
+
70
+ An extended project starts with the packages and one command:
71
+
72
+ ```sh
73
+ npm install @jimhoyd/urlcode @jimhoyd/urlcode-ui @jimhoyd/urlcode-auth @jimhoyd/urlcode-admin
74
+ urlcode init my-site --with auth,admin
75
+ ```
76
+
77
+ Installing from npm is the normal path; the extension repositories'
78
+ `pack-sources.mjs` still builds local tarballs from a reviewed checkout for
79
+ operators who install only source they have read. Three files make an extended
80
+ project. Nothing else is discovered by convention.
81
+
82
+ ```
83
+ site/
84
+ urlcode.yaml the project: routes, extensions, policies (Git-owned, untrusted content)
85
+ functions/, public/ guest code and assets referenced from the YAML
86
+ operator/
87
+ host.mjs trusted operator code: default-exports { extensions, plugins?, close? }
88
+ operator-service.mjs opens the auth store, keys and senders; imported by host.mjs
89
+ data/ private: auth.sqlite, encryption key, CSRF key
90
+ ```
91
+
92
+ The project declares logical extensions and exclusive mounts:
93
+
94
+ ```yaml
95
+ version: "1"
96
+ extensions:
97
+ ui: { version: "1", config: { theme: { name: Acme, colors: { primary: "24 95% 53%" } } } }
98
+ auth: { version: "1", config: { registration: "off" } }
99
+ admin: { version: "1", config: {} }
100
+ routes:
101
+ /assets/ui/*: { extension: ui, methods: [GET, HEAD] }
102
+ /account/*: { extension: auth, methods: [GET, HEAD, POST] }
103
+ /admin/*: { extension: admin, methods: [GET, HEAD, POST] }
104
+ /private:
105
+ respond: { text: Signed in }
106
+ policies:
107
+ extensions:
108
+ auth: {}
109
+ ```
110
+
111
+ The operator host wires the packages, once, in code the project cannot reach:
112
+
113
+ ```js
114
+ import { createUiExtension } from '@jimhoyd/urlcode-ui/host';
115
+ import { authExtension, createPresentation, englishCatalogue } from '@jimhoyd/urlcode-auth';
116
+ import { adminExtension } from '@jimhoyd/urlcode-admin';
117
+ import { service, csrfKey, projectSha256 } from './operator-service.mjs';
118
+
119
+ const ui = createUiExtension({ projectSha256, projectRoot: '/absolute/site', sources: [englishCatalogue] });
120
+ const presentation = createPresentation({ theme: { '--ui-accent': '#0645ad' } });
121
+ export default {
122
+ extensions: [
123
+ ui.registration,
124
+ authExtension({ service, csrfKey, projectSha256, presentation }),
125
+ adminExtension({ service, csrfKey, projectSha256, authMount: '/account', presentation }),
126
+ ],
127
+ async close() { await service.close(); },
128
+ };
129
+ ```
130
+
131
+ Auth and admin pages currently render through the shared primitives and a
132
+ `presentation` (catalogue and `--ui-*` theme variables). The `ui` extension
133
+ serves the template kit and its assets for pages an extension renders through
134
+ `ui.kit`; moving the auth and admin screens onto the kit is a listed remaining
135
+ item in the ui repository, so today one `presentation` restyles them and the
136
+ `ui` block is optional.
137
+
138
+ ```sh
139
+ urlcode serve --project /absolute/site --host-file /absolute/operator/host.mjs --origin https://site.example
140
+ ```
141
+
142
+ `urlcode init <dir> --with auth,admin` writes this layout in one step: it
143
+ resolves each installed `@jimhoyd/urlcode-<name>` from the current directory,
144
+ calls its `scaffold` export and merges the fragments into `app/urlcode.yaml`,
145
+ one `host.mjs` and one `README.md`, refusing before writing anything when a
146
+ package is missing or two fragments collide (the contract is documented under
147
+ [scaffolding](EXTENSIONS.md#scaffolding-with-init---with)). `urlcode-auth init`
148
+ and `urlcode-admin init` write the same layout for a single package; `urlcode-auth bootstrap` creates the first
149
+ administrator from JSON on stdin. `inspectExtensionRevision(project)` prints
150
+ the SHA-256 that `projectSha256` must carry; changing extension YAML, policies
151
+ or mounts changes the revision and needs an explicit operator reapproval.
152
+
153
+ ## Rules an agent must follow
154
+
155
+ These are the facts that keep generated projects valid. The full matrix is in
156
+ [AI authoring](AI-AUTHORING.md); this is the short list.
157
+
158
+ - **YAML never names code, packages, databases or credentials.** Extensions are
159
+ logical names; the host file chooses the implementation. There is no
160
+ `--extension` flag, no `import` in YAML, no interpolation.
161
+ - **One handler per route.** `redirect`, `respond`, `page`, `static`, `download`,
162
+ `function`, `proxy`, `conditional` or `extension`, plus optional
163
+ `middleware`. Paths are exact or single-segment `{param}`; `/*` only on
164
+ `static` and `extension` mounts. No regex.
165
+ - **`function`/`middleware` code is trusted by default, sandboxed opt-in.**
166
+ It runs in-process with full Node access unless the route declares
167
+ `sandbox: true`, which isolates it to a text/JSON `Request`/`Response`
168
+ subset, validated `args` and granted `env`, with no `fetch`, Node,
169
+ filesystem or timers. Either way, `args`/`env`/`secrets` are exactly what
170
+ the route declares and an operator grants — trust changes where code runs,
171
+ not what it is handed. See docs/SPIKE-DEFAULT-TRUST-MODEL.md and
172
+ docs/FUNCTION-SECURITY.md.
173
+ - **Authentication is host processing.** Do not build login forms, session
174
+ cookies or password checks in functions. Declare `policies.extensions.auth`
175
+ on the route; the runtime withholds `Cookie` and `Authorization` from guests.
176
+ - **Everything is validated before it runs.** `urlcode validate --local`,
177
+ `urlcode test`, `urlcode audit --expect-routes N`. Unsupported features fail
178
+ with the route named; nothing degrades silently.
179
+ - **Provider targets refuse what they cannot enforce.** Cloudflare runs
180
+ redirects and declared responses only. Serverless adapters refuse functions,
181
+ proxy, signals and extensions. The `static` target (S3 + CloudFront,
182
+ no server) refuses everything that needs request-time logic, keeping only
183
+ `redirect`/`respond`/`page`/`static`/`download` — see [static
184
+ hosting](STATIC.md). Check `urlcode capabilities --target NAME` before
185
+ promising a deployment.
186
+ - **Report evidence, not hope.** The commands above are the evidence. Local
187
+ tests are not deployment, soak or independent security review.
188
+
189
+ ## Where to read next
190
+
191
+ | Need | Read |
192
+ |---|---|
193
+ | Write or change routes | [YAML guide](YAML-GUIDE.md), [field reference](YAML-REFERENCE.md), [cookbook](../examples/cookbook/README.md) |
194
+ | Add accounts | [auth README](https://github.com/jimhoyd-com/urlcode-auth#readme), [auth security](https://github.com/jimhoyd-com/urlcode-auth/blob/main/SECURITY.md) |
195
+ | Add administration | [admin README](https://github.com/jimhoyd-com/urlcode-admin#readme) |
196
+ | Restyle every page | [ui README](https://github.com/jimhoyd-com/urlcode-ui#readme), [ui contract](https://github.com/jimhoyd-com/urlcode-ui/blob/main/CONTRACT.md) |
197
+ | Write an extension | [extensions](EXTENSIONS.md), [extension model review](SPIKE-EXTENSION-MODEL.md) |
198
+ | Run it | [operations](OPERATIONS.md), [install](INSTALL.md), [deployment checks](DEPLOYMENT-CHECKS.md) |