@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
@@ -1,11 +1,46 @@
1
- # Untrusted function execution
2
-
3
- Application code is untrusted even when it came from your own Git repository.
4
- A compromised dependency, template or contribution must not inherit the URLCode
5
- server's authority. Alpha.2 replaces alpha.1's Node execution entirely. There
6
- is no `unsafe`, `trusted` or automatic host-execution fallback.
7
-
8
- ## Boundaries enforced now
1
+ # Function execution: trusted by default, sandboxed opt-in
2
+
3
+ `function` and `middleware` routes run **trusted and unsandboxed by default**:
4
+ in the host process, with full Node, filesystem and network access, exactly
5
+ like any other project code (docs/SPIKE-DEFAULT-TRUST-MODEL.md). This is a
6
+ deliberate, maintainer-decided reversal of alpha.2's blanket sandbox — see
7
+ that spike document for the full rationale. It is a call the project makes,
8
+ not a property the runtime can verify: URLCode cannot know whether your code
9
+ is safe to trust, only whether you asked for isolation.
10
+
11
+ Declare `sandbox: true` on a route when its code specifically warrants
12
+ isolation: it processes input from a source the project doesn't fully trust
13
+ (a third-party webhook payload, for example), it is a contribution nobody on
14
+ the team has reviewed, or it handles a secret sensitive enough that a bug in
15
+ that one route should not be able to reach the rest of the process or the
16
+ filesystem. A sandboxed route runs in QuickJS inside WebAssembly, in a
17
+ separate worker thread, with none of the host access described below — its
18
+ guarantees are unchanged from every earlier release and are described in
19
+ full in the rest of this document. Absence of `sandbox` (or `sandbox: false`)
20
+ means trusted; there is no separate `unsafe`/`trusted` field to opt back into
21
+ the old sandboxed-by-default behavior — set `sandbox: true` per route instead.
22
+
23
+ **Either way, binding grants are unaffected.** Trusting a route's code by
24
+ default does not grant it any `env`/`secrets` it was not explicitly declared
25
+ in YAML and approved by an operator policy pinned to the project revision
26
+ (see "Granting selected bindings" below). A trusted function only *can* do
27
+ more with Node once it runs — it does not receive anything more than a
28
+ sandboxed one would.
29
+
30
+ This is a claim about `context`/`context.secrets` injection, not an
31
+ access-control guarantee on trusted code. The binding grant governs only what
32
+ URLCode hands a route through `context`; it does not restrict what trusted
33
+ (non-`sandbox`) code can independently do, because that code has full Node
34
+ access by design. A trusted function can read `process.env`, open files or
35
+ make network calls on its own regardless of what its route was or was not
36
+ granted — withholding a binding grant limits what URLCode gives the code
37
+ through `context`, not what the code itself, running with full Node access,
38
+ can go and get. A sandboxed route has no such independent access: the guest
39
+ API is all it has, so its binding grant *is* effectively its whole reach into
40
+ the environment. Trusted code's reach is not bounded that way; treat the
41
+ grant as scoping `context`, not as scoping the process.
42
+
43
+ ## What "sandboxed" (`sandbox: true`) still guarantees
9
44
 
10
45
  - Function sources are parsed/snapshotted without importing them into Node.
11
46
  - Code runs in QuickJS inside WebAssembly, with no host JS functions/objects
@@ -22,9 +57,64 @@ is no `unsafe`, `trusted` or automatic host-execution fallback.
22
57
  Operator grants are exact-name, route-scoped and pinned to configuration/source.
23
58
 
24
59
  The guest API is intentionally narrower than Node or full Fetch; see the
25
- [implemented contract](SPECIFICATION.md). Existing functions using Node/network
26
- or binary/stream APIs must be rewritten for the supported profile or wait for a
27
- reviewed capability implementation. Redirects need none of this machinery.
60
+ [implemented contract](SPECIFICATION.md). A function moving from trusted to
61
+ `sandbox: true` that uses Node/network or binary/stream APIs must be rewritten
62
+ for the supported guest profile, or stay trusted. Redirects need none of this
63
+ machinery either way.
64
+
65
+ This engine — worker spawning, the module-allowlist walk, the two-layer
66
+ deadline, `maxBytes` and response-shape validation — is one implementation
67
+ shared by route dispatch and by `@jimhoyd/urlcode/sandbox`'s `SandboxPool`,
68
+ the public primitive an extension package uses to run a project-supplied hook
69
+ through this exact isolation when the project's own config declares
70
+ `sandbox: true` on it (see [EXTENSIONS.md](EXTENSIONS.md#project-level-lifecycle-hooks)
71
+ and [TYPESCRIPT.md](TYPESCRIPT.md)). Every guarantee above applies identically
72
+ through that entry point; there is no separate, weaker sandbox for extensions
73
+ to reach for, and no "trusted" mode exported there — trusted execution needs
74
+ no primitive at all (docs/SPIKE-DEFAULT-TRUST-MODEL.md).
75
+
76
+ ## What the trusted default can and can't do
77
+
78
+ A trusted route (no `sandbox`, or `sandbox: false`) has none of the guest
79
+ restrictions above:
80
+
81
+ - Full Node built-ins, `process`, the filesystem, `fetch`, sockets, workers
82
+ and npm packages are available, exactly as in any other Node module.
83
+ - Module resolution is ordinary Node ESM resolution: bare specifiers, dynamic
84
+ `import()` and node_modules all work. There is no dependency-graph allowlist
85
+ and no per-module/total source-size budget (function-sources.ts's
86
+ `MODULE_LIMIT`/`MODULE_BYTE_LIMIT`/`TOTAL_BYTE_LIMIT` apply only to what a
87
+ sandboxed snapshot bundles).
88
+ - Node's own module cache is shared across invocations and across the whole
89
+ process; there is no fresh heap per call. Module-level state persists
90
+ between requests exactly like an ordinary long-running Node server, so a
91
+ trusted function that mutates shared/global state affects later requests
92
+ the way hand-written server code would.
93
+ - There is no worker-thread deadline that force-terminates a stuck call. A
94
+ trusted invocation races a configurable timeout, but that race can only
95
+ reject the *call*; it cannot preempt code that blocks the event loop
96
+ synchronously. See [capacity](CAPACITY.md) for what this means for one slow
97
+ or hung trusted route's effect on the rest of the process.
98
+ - A snapshot reload re-imports a trusted route's own entry file fresh (each
99
+ reload gets its own cache-busted module registration), so editing the
100
+ `source` file a route declares and reloading picks up the change, the same
101
+ as the sandboxed pool rebuilding from scratch. A file that entry file
102
+ merely *imports* is not similarly busted: Node's own module cache is
103
+ keyed by the resolved URL of that import statement, which this runtime
104
+ does not rewrite, so an edited dependency two files deep from the route
105
+ keeps serving its old content until the process restarts. Restructure a
106
+ route so the code you expect to hot-reload is the declared entry file
107
+ itself, or restart rather than reload after editing a trusted route's
108
+ dependencies. A `sandbox: true` route has no such gap: reload always
109
+ rebuilds its whole snapshot, dependencies included.
110
+
111
+ What does **not** change with trust: `args` are still exactly the validated
112
+ values the route declares (never raw request input), and `env`/`secrets` are
113
+ still exactly what the route's YAML requests and an operator policy grants,
114
+ pinned to the project revision — trust changes where code runs, not what
115
+ it is handed *through `context`*. It does not change what the code can go get
116
+ on its own once it is running; see "binding grants are unaffected" above for
117
+ that distinction.
28
118
 
29
119
  ## Granting selected bindings
30
120
 
@@ -63,26 +153,28 @@ urlcode serve --project /srv/my-links --policy /etc/urlcode/my-links-policy.json
63
153
 
64
154
  `dev`, `test` and `validate --local` use the same policy rules even for `.env.local`.
65
155
  The JavaScript API accepts an equivalent operator-supplied `permissions` object.
66
- Every config/module change invalidates the grant; inspect/review the new revision
67
- before updating the operator file. Policies are read at startup, not hot-reloaded.
68
- A failed development candidate leaves the previous approved snapshot running.
156
+ Every config change invalidates the grant, and so does a module change within
157
+ what the approval digest actually hashes: for a sandboxed route, its
158
+ middleware/function sources and their full dependency graph; for a trusted
159
+ route, only its own entry-file source (see the next paragraph — a trusted
160
+ route's transitive dependencies are explicitly **not** part of that digest).
161
+ Inspect/review the new revision before updating the operator file. Policies
162
+ are read at startup, not hot-reloaded. A failed development candidate leaves
163
+ the previous approved snapshot running.
69
164
 
70
165
  Granting a secret deliberately makes it available to every middleware and function
71
- in that route. Middleware sources and their dependencies are included in the
72
- approval digest; changes invalidate grants. The whole chain shares one fresh
73
- guest heap and one execution deadline. Code can
74
- include any granted data in its HTTP response. A sandbox cannot promise secrecy
75
- from code authorized to read a value. Minimize grants, use scoped/short-lived
76
- credentials and revoke/restart when needed. Other routes get none of that context.
77
-
78
- ## Native live-link storage
79
-
80
- The optional `link` handler can read an explicitly operator-bound collection.
81
- Its database is outside the project and public serving opens it read-only. This
82
- is a native handler, not a guest capability: functions/middleware receive no SQL,
83
- filesystem handle, database path or management token. Native link data changes do
84
- not authorize new code or bindings. Management requires a separate operator CLI
85
- or token-protected listener. See [dynamic links](DYNAMIC-LINKS.md).
166
+ in that route, trusted or sandboxed alike. A sandboxed route's middleware
167
+ sources and their full dependency graph are included in the approval digest,
168
+ as before; a trusted route's own entry-file source is included too, so
169
+ changing that file's content invalidates the grant, but a change to a helper
170
+ module it merely imports does not by itself (see function-sources.ts's
171
+ `collectTrustedSources`) a known, documented gap versus the sandboxed path's
172
+ full dependency-graph hashing: a trusted route's grant scope is entry-file-only,
173
+ not transitive. Either way, code can include any granted data
174
+ in its HTTP response: neither the sandbox nor the trusted default promises
175
+ secrecy from code that was explicitly authorized to read a value. Minimize
176
+ grants, use scoped/short-lived credentials and revoke/restart when needed.
177
+ Other routes get none of that context.
86
178
 
87
179
  ## Next capability work
88
180
 
@@ -90,9 +182,14 @@ Outbound requests need a host-owned broker with explicit destination/method
90
182
  allowlists, private/metadata/loopback-address restrictions, DNS/rebinding defenses,
91
183
  redirect revalidation, deadlines and byte/concurrency limits. Application YAML
92
184
  must not grant those permissions. Persistent state needs similarly scoped access.
93
- Until such brokers are implemented and tested, these capabilities are unavailable.
94
- Provider adapters must preserve this boundary or reject deployment; they cannot
95
- silently replace sandbox execution with unrestricted Node functions.
185
+ Until such brokers are implemented and tested, these capabilities are unavailable
186
+ to a *sandboxed* route. Provider adapters must preserve a `sandbox: true`
187
+ route's isolation or reject deployment; they cannot silently downgrade a
188
+ route that explicitly asked for the sandbox into unrestricted Node execution.
189
+ (A trusted route, by contrast, already has unrestricted Node execution by
190
+ design on the self-hosted target — see "What the trusted default can and
191
+ can't do" above; non-Node targets refuse `function`/`middleware` entirely,
192
+ trusted or sandboxed, since neither execution mode exists there.)
96
193
 
97
194
  ## Verification and remaining risk
98
195
 
package/docs/INSTALL.md CHANGED
@@ -6,34 +6,54 @@ pick whichever fits how you already manage tools. All of them require
6
6
  TypeScript source (`dist/`, with declarations; see [TypeScript](TYPESCRIPT.md)),
7
7
  so the installed `urlcode` command runs `dist/cli.js` and needs no build tool.
8
8
 
9
- Live short-link storage additionally needs a Node build carrying the patched
10
- SQLite WAL fix. Run `urlcode doctor` after installing and check `liveLinks`;
11
- everything except live links works on any supported build. See
12
- [dynamic links](DYNAMIC-LINKS.md#node-build-requirement).
13
-
14
9
  ## npm
15
10
 
16
11
  ```sh
17
- npm install --global urlcode
12
+ npm install --global @jimhoyd/urlcode
18
13
  urlcode --help
19
14
  ```
20
15
 
21
- Project-local, which is what an application repository should normally pin:
16
+ Project-local, which is what an application repository should normally pin.
17
+ Which dependency list it belongs in depends on how the project uses URLCode:
22
18
 
23
19
  ```sh
24
- npm install --save-dev urlcode
20
+ # Using URLCode as a tool: validate, test and build in CI, never imported by
21
+ # the code that serves requests.
22
+ npm install --save-dev @jimhoyd/urlcode
25
23
  npx urlcode validate
24
+
25
+ # Embedding the runtime (see TYPESCRIPT.md): the application imports
26
+ # @jimhoyd/urlcode at startup, so it must survive `npm ci --omit=dev`.
27
+ npm install --save @jimhoyd/urlcode
26
28
  ```
27
29
 
30
+ A devDependency is absent from a production install, so an application that
31
+ imports `createRuntime`, `startServer`, `prerenderPages` or any other
32
+ [embedding entry point](TYPESCRIPT.md) fails at startup on a missing module if it
33
+ is installed with `--save-dev`. An application should also pin an **exact**
34
+ version rather than a range: the compiled Cloudflare artifact format is tied to
35
+ the runtime version that reads it.
36
+
28
37
  ## Homebrew
29
38
 
30
39
  ```sh
31
40
  brew tap jimhoyd-com/urlcode
41
+ brew trust jimhoyd-com/urlcode
32
42
  brew install urlcode
33
43
  ```
34
44
 
45
+ Homebrew refuses to load a formula from a third-party tap until you trust it,
46
+ so without the middle line the install stops with `Refusing to load formula …
47
+ from untrusted tap`. Trusting a tap means agreeing to run code from this
48
+ repository, the same as with the install script; `brew trust --formula
49
+ jimhoyd-com/urlcode/urlcode` limits it to this one formula.
50
+
35
51
  The tap's formula is generated from the published tarball for each release and
36
- attached to the GitHub release as `urlcode.rb`.
52
+ attached to the GitHub release as `urlcode.rb`. Homebrew verifies the tarball's
53
+ SHA-256 against the formula before installing.
54
+
55
+ Verified on macOS (arm64) against the 0.3.0 tap: fetch verified, installed into
56
+ `/opt/homebrew/Cellar/urlcode/0.3.0`.
37
57
 
38
58
  ## Install script
39
59
 
@@ -45,7 +65,7 @@ It downloads the release tarball, verifies its SHA-256 against the release's
45
65
  `SHA256SUMS`, and installs with npm. Options:
46
66
 
47
67
  ```sh
48
- curl -fsSL .../install.sh | sh -s -- --version 0.2.0 --prefix "$HOME/.local"
68
+ curl -fsSL .../install.sh | sh -s -- --version 0.3.0 --prefix "$HOME/.local"
49
69
  ```
50
70
 
51
71
  `--prefix` avoids needing privileges for a global npm directory; add
@@ -54,8 +74,14 @@ for that moment: to inspect first, download it, read it, then run it.
54
74
 
55
75
  ## Container
56
76
 
77
+ No image is published yet: the release job's GHCR step is gated behind the
78
+ `PUBLISH_CONTAINER` repository variable and has not run, so there is nothing at
79
+ `ghcr.io/jimhoyd-com/urlcode` to pull. Build it from a release checkout:
80
+
57
81
  ```sh
58
- docker run --rm -p 127.0.0.1:3000:3000 -v "$PWD:/project:ro" ghcr.io/jimhoyd-com/urlcode:0.2.0 \
82
+ git clone --branch v0.3.0 https://github.com/jimhoyd-com/urlcode.git
83
+ docker build -t urlcode:0.3.0 urlcode
84
+ docker run --rm -p 127.0.0.1:3000:3000 -v "$PWD:/project:ro" urlcode:0.3.0 \
59
85
  serve --project /project --host 0.0.0.0
60
86
  ```
61
87
 
@@ -74,13 +100,20 @@ make dev
74
100
  A clone runs the TypeScript source directly (`node src/cli.ts`, Node 22.18+),
75
101
  with no build step; see [local development](LOCAL-DEVELOPMENT.md).
76
102
 
103
+ Three Node versions appear around the project, and they are not a contradiction:
104
+ the installed package runs on Node 22.13 or newer (`engines`), running the
105
+ TypeScript source from a clone needs 22.18 or newer because it relies on Node's
106
+ built-in type stripping, the release workflow's npm trusted publishing needs
107
+ 22.14 or newer, and the container image pins Node 26. Only the first number
108
+ constrains a deployment of the published tarball.
109
+
77
110
  ## Verify what you installed
78
111
 
79
112
  Releases carry Sigstore provenance signed by the release workflow. Before
80
113
  trusting a downloaded artifact:
81
114
 
82
115
  ```sh
83
- gh attestation verify urlcode-0.2.0.tgz --repo jimhoyd-com/urlcode \
116
+ gh attestation verify jimhoyd-urlcode-0.3.0.tgz --repo jimhoyd-com/urlcode \
84
117
  --signer-workflow jimhoyd-com/urlcode/.github/workflows/release.yml
85
118
  ```
86
119
 
@@ -0,0 +1,134 @@
1
+ # Redirect interchange and bulk authoring
2
+
3
+ `importRoutes` and `exportRoutes` return a conversion report without writing files,
4
+ activating a runtime, reading bindings, loading function sources or running code.
5
+ The CLI can preview the report before creating a new output file. Failed reports
6
+ never contain a partial document or output; duplicate paths never overwrite a row.
7
+
8
+ ```js
9
+ import { importRoutes, exportRoutes } from '@jimhoyd/urlcode';
10
+ const report = await importRoutes({
11
+ format: 'csv', source: 'migration.csv',
12
+ text: 'path,url,status\n/old,https://example.test/new,301\n'
13
+ });
14
+ if (!report.ok) throw new Error(JSON.stringify(report.diagnostics));
15
+ // report.output is URLCode project YAML; report.document is the validated project.
16
+ const csv = await exportRoutes({ format: 'csv', document: report.document });
17
+ ```
18
+
19
+ Each report has `ok`, `lossless`, `routeCount` and `diagnostics`. Diagnostics name
20
+ an input source, physical CSV/text row or JSON/YAML array index where available,
21
+ a path where appropriate, a code, severity and explanation. Destinations and
22
+ credentials are not echoed in validation errors. Output is sorted by literal
23
+ path, independent of input order. Import does not merge an existing project;
24
+ normal project loading rejects collisions across includes when output is added.
25
+
26
+ ## CLI usage
27
+
28
+ ```sh
29
+ # Bulk row formats can be inferred from the input extension or named explicitly.
30
+ urlcode import csv migration.csv --out routes.yaml --dry-run
31
+ urlcode import csv migration.csv --out routes.yaml
32
+ urlcode import migration.json --report json --dry-run
33
+
34
+ # Provider migrations require explicit acknowledgment of the reported differences.
35
+ urlcode import netlify _redirects --out imported.yaml --dry-run
36
+ urlcode import netlify _redirects --out imported.yaml --accept-provider-differences
37
+ urlcode export --target netlify --project ./project --out _redirects --accept-provider-differences
38
+ urlcode export --target cloudflare --project ./project --dry-run
39
+ urlcode export --target vercel --project ./project --dry-run
40
+ urlcode export --target netlify-toml --project ./project --dry-run
41
+ ```
42
+
43
+ General import syntax is `urlcode import [format] FILE`; `--format` explicitly
44
+ overrides the positional format or extension. Exports use `--target` for a
45
+ provider format, or `--format csv|json|yaml` for row data. `--out FILE` creates a
46
+ new file with mode 0600 and never overwrites one. `--dry-run` validates and
47
+ returns a report without creating the requested output. `--report json` always
48
+ emits the report. Failures exit nonzero and leave no converted output.
49
+
50
+ Reports include source/row diagnostics and the successful candidate output.
51
+ Acknowledged provider conversions always emit a report with their warnings,
52
+ even when `--out` is provided; raw provider output is never silently printed as
53
+ if it were lossless. Project export flattens validated includes, but refuses
54
+ other unsupported project behavior instead of dropping it. For a large input
55
+ that needs a complete sharded project, use [bulk import](BULK.md).
56
+
57
+ ## Supported forms
58
+
59
+ | Format | Input/output subset |
60
+ | --- | --- |
61
+ | `csv` | Header exactly `path,url,status`; optional empty status defaults to 302; quoted fields and escaped quotes |
62
+ | `json` | Array of `{path,url,status?}`; status must be a number |
63
+ | `yaml` | The same row array in strict URLCode YAML syntax; not a full project file |
64
+ | `netlify` | `_redirects`: literal source, absolute HTTP(S) destination, optional status (default 301) |
65
+ | `cloudflare` | Pages `_redirects`: same columns, default 302; 2,000 static rules, 1,000 characters per rule |
66
+ | `vercel` | `vercel.json` containing only `redirects`; literal `source`, absolute `destination`, and either boolean `permanent` (308/307) or `statusCode` |
67
+ | `netlify-toml` | Only `[[redirects]]` blocks with `from`, `to`, optional numeric `status` (default 301); unescaped double-quoted strings |
68
+
69
+ All statuses are restricted to 301, 302, 303, 307 and 308. Literal ASCII paths
70
+ and absolute HTTP(S) destinations are required. The existing schema and semantic
71
+ compiler validate resulting routes. No provider pattern compiler is introduced.
72
+
73
+ The TOML subset deliberately rejects general TOML constructs, build settings,
74
+ escapes, inline comments, nested conditions, force flags and other tables. A full
75
+ `netlify.toml` must be separated into a redirects-only input by the operator. This
76
+ avoids silently discarding build settings or reinterpreting unsupported syntax.
77
+
78
+ Functions, assets, middleware, conditions, parameters, query maps/allowlists,
79
+ headers, policies, includes and any other extra route/project fields cannot be
80
+ exported by this subset. Relative destinations, wildcards, provider placeholder
81
+ syntax, rewrites and duplicate paths fail. Use a runtime adapter for richer
82
+ behavior. CSV/JSON/YAML round-trips are lossless within the declared simple subset.
83
+
84
+ ## Provider semantics require explicit acknowledgment
85
+
86
+ Provider conversions fail by default. `acceptProviderDifferences: true` (CLI
87
+ `--accept-provider-differences`) permits a **non-lossless migration candidate**;
88
+ it does not suppress invalid rules, discard unsupported fields or claim exact
89
+ behavior. Every such successful report retains a warning and `lossless: false`.
90
+
91
+ URLCode drops incoming query parameters and defaults to GET/HEAD. Native
92
+ redirect systems can apply to other methods and have different normalization
93
+ and request-query behavior. Netlify automatically forwards queries for common
94
+ redirect statuses and can give existing files precedence. Cloudflare Pages
95
+ redirects override assets, but Pages Functions can bypass `_redirects`.
96
+ Review behavior for requests with queries, non-GET methods, slash/URL normalization
97
+ and conflicting assets/functions before deploying. The acknowledged subset is
98
+ literal GET/HEAD requests without these conflicts. No provider deployment was
99
+ performed as part of interchange unit tests.
100
+
101
+ Provider references checked 2026-09-17:
102
+
103
+ - [Netlify redirect options](https://docs.netlify.com/manage/routing/redirects/redirect-options/)
104
+ - [Netlify TOML configuration](https://docs.netlify.com/build/configure-builds/file-based-configuration/)
105
+ - [Cloudflare Pages redirects](https://developers.cloudflare.com/pages/configuration/redirects/)
106
+ - [Vercel redirect configuration](https://vercel.com/docs/project-configuration/vercel-json)
107
+
108
+ ## Resource limits
109
+
110
+ Imports reject more than 32 MiB of input or 100,000 rows. Diagnostic collection
111
+ stops after 100 invalid rows; normal route compilation retains its deadline and
112
+ validation rules. Large output can still exceed the runtime YAML worker's heap
113
+ or deadline: conversion success does not promise activation of a single 100k
114
+ route YAML file. Split large migration inputs into independently reviewed files
115
+ and use explicit `includes`; aggregate project limits continue to apply.
116
+
117
+ Reports also expose bounded classification counts:
118
+
119
+ - `convertedRoutes`: routes actually returned in successful output; zero when
120
+ any error prevents output.
121
+ - `nativeEquivalentRoutes`: returned routes with no conversion warnings; always
122
+ zero for acknowledged provider conversions.
123
+ - `runtimeRequiredRoutes`: route diagnostics identifying behavior outside the
124
+ simple export subset.
125
+ - `unsupportedRows`: known invalid/duplicate input-row diagnostics.
126
+ - `providerDifferenceRoutes`: candidate routes subject to the provider semantic
127
+ warning; these overlap converted routes after acknowledgment.
128
+ - `fullyScanned`: false on parser/global failures or truncated diagnostics. Counts
129
+ then describe only examined input, never an inferred total.
130
+
131
+ These counts are not a partition of arbitrary malformed input. `routeCount`
132
+ retains its original meaning: routes in the candidate table, or rows
133
+ parsed before an early global failure. A failed report contains no candidate
134
+ output even if some rows were convertible.
@@ -75,8 +75,8 @@ largest one that makes a number go green.
75
75
 
76
76
  ## What a passing run does not prove
77
77
 
78
- - **GET and HEAD only.** Function routes with request bodies, live-link writes
79
- and the management API are not exercised. `workload` states this in every report.
78
+ - **GET and HEAD only.** Function routes with request bodies are not exercised.
79
+ `workload` states this in every report.
80
80
  - **Redirects are not followed**, so a redirect's destination is never loaded.
81
81
  - **One client, one host, no slow peers.** Tail latency under adversarial
82
82
  clients, connection churn or packet loss is not measured.
@@ -87,5 +87,5 @@ largest one that makes a number go green.
87
87
  runtime and application revisions, host, and command with any figure you keep.
88
88
 
89
89
  `scripts/operational-drills.ts` covers the adjacent ground — mixed
90
- native/function/live-link load, an invalid reload, rollback and a quiesced
91
- backup/restore — as a local proof, never a statement about production.
90
+ native/function load, an invalid reload and rollback as a local proof,
91
+ never a statement about production.
@@ -0,0 +1,75 @@
1
+ # Middleware examples
2
+
3
+ Fourteen reusable middleware modules covering fifteen patterns (`auth.mjs`
4
+ exports both `bearer` and `basic`), each in
5
+ [`examples/cookbook/middleware`](../examples/cookbook/middleware) with a route in
6
+ [`routes/middleware.yaml`](../examples/cookbook/routes/middleware.yaml) and request
7
+ fixtures in the cookbook tests. The same modules ship as the `middleware`
8
+ [local recipe](RECIPES.md):
9
+
10
+ ```sh
11
+ urlcode recipes add middleware --out ./my-middleware
12
+ urlcode test --project ./my-middleware
13
+ ```
14
+
15
+ They cover what Express, Koa, Fastify, Hono, Next.js and edge runtimes usually
16
+ ship as middleware, rewritten for URLCode's [guest API](MIDDLEWARE.md): text/JSON
17
+ `Request`/`Response`, `Headers`, `context.inputs/args/env/secrets/state`, timers
18
+ and nothing else. There is no `crypto`, `URL`, `fetch`, storage or console, and
19
+ `context.state` dies with the request. Patterns that need any of those are listed
20
+ at the end so nobody wastes time porting them.
21
+
22
+ | Pattern | Module | Framework equivalent | Demonstrates |
23
+ | --- | --- | --- | --- |
24
+ | Bearer token gate | `auth.mjs` `bearer` | Hono `bearerAuth`, `express-bearer-token` | Early 401, `www-authenticate`, constant-time compare against an `env` binding |
25
+ | Basic authentication | `auth.mjs` `basic` | Hono `basicAuth`, `express-basic-auth` | Hand-written base64 (no `atob`), both checks always evaluated, `state.user` |
26
+ | CORS | `cors.mjs` | Express `cors`, Hono `cors` | `OPTIONS` answered before the handler, origin allowlist, `vary: origin` |
27
+ | Correlation id and timing | `request-id.mjs` | Hono `requestId`/`timing`, Express `response-time` | Validating a caller header, `server-timing`, coexisting with the runtime's own `x-request-id` |
28
+ | Maintenance switch | `maintenance.mjs` | Next.js and Netlify Edge maintenance examples | 503 with `retry-after`, bypass header, flipping behavior from a binding |
29
+ | Error boundary | `errors.mjs` | Koa `onerror`, Express error handlers | Catching a downstream throw, JSON 500 instead of a bare 502 |
30
+ | JSON envelope | `envelope.mjs` | Response transformers | Reading a function body once, passing native bodies through untouched |
31
+ | Content negotiation | `negotiate.mjs` | Express `res.format` | Parsing `accept` with q-values, 406, `vary: accept` |
32
+ | Method override | `methods.mjs` `override` | Express `method-override` | Bounded tunneling through POST, 405 with `allow` |
33
+ | ETag and 304 | `etag.mjs` | Express `etag`, Fastify `@fastify/etag` | FNV-1a weak tag, `if-none-match`, null-body 304 |
34
+ | A/B bucket | `bucket.mjs` | Vercel and Cloudflare A/B examples | Cookie parsing, `set-cookie`, replacing a native redirect |
35
+ | Locale redirect | `locale.mjs` | Next.js i18n middleware | `accept-language` ranking, allowlisted languages, `vary` |
36
+ | Referer allowlist | `referer.mjs` | Hotlink protection rules | Gating a native download without reading it |
37
+ | Body validation | `body.mjs` | `express-validator`, Fastify schemas | Single-use body, 422 error list, handoff through `state` |
38
+ | Debug echo | `debug.mjs` | Request loggers | Inspecting inputs, args and redacted headers when the console is silent |
39
+
40
+ ## Reading the modules
41
+
42
+ Every module follows the shape in [middleware](MIDDLEWARE.md): read the request,
43
+ optionally return early, otherwise `await next()` once and return a `Response`.
44
+ Three habits recur and are worth copying:
45
+
46
+ - **Configuration lives in bindings.** Tokens, allowlists and switches are read
47
+ from `context.env`. The cookbook binds literal values so it runs without grants;
48
+ a deployed project uses `{secret: name}` for credentials and an operator grant.
49
+ - **Native bodies stay opaque.** `envelope`, `negotiate` and `etag` only rewrite a
50
+ body when the downstream response is a function response with a readable
51
+ content type. `bucket`, `locale` and `referer` wrap native redirects and
52
+ downloads without touching their bytes; to change the destination they return
53
+ a new `Response` instead.
54
+ - **Chains compose through `state`.** `/fragile` runs `request-id` before
55
+ `errors`, so the fallback JSON carries the correlation id. `/profile` parses
56
+ the body once in middleware and the function reads `context.state.body`.
57
+
58
+ ## Limits these examples respect
59
+
60
+ - **No `crypto`.** The auth modules compare a shared token; they cannot verify
61
+ HMAC signatures, JWTs or password hashes. Signed URLs would be a runtime
62
+ feature, not a middleware example.
63
+ - **No cross-request state.** Rate limiting, caching, sessions and CSRF tokens
64
+ need storage the guest does not have. Throttling and cache headers exist as
65
+ native [policies](POLICIES.md) instead.
66
+ - **No logging target.** The guest console is a no-op; `debug.mjs` returns the
67
+ information to the caller instead, and only when both a binding and a header
68
+ ask for it. Remove that route before publishing a project.
69
+ - **Runtime headers win.** The runtime stamps `x-request-id` on every response,
70
+ which is why the tracing example uses `x-correlation-id`. YAML
71
+ `response.headers` also override middleware headers.
72
+
73
+ Each cookbook route has fixtures for its success path, its early responses, its
74
+ validation failures and every declared method, which is what `urlcode audit`
75
+ expects before it reports a middleware-wrapped route as covered.
@@ -2,7 +2,10 @@
2
2
 
3
3
  Middleware is reusable JavaScript around any route handler. It is optional and
4
4
  route-local; plain redirects and assets retain their native fast path when no
5
- middleware is attached. Adding middleware requires sandbox execution.
5
+ middleware is attached. Middleware runs trusted and unsandboxed by default,
6
+ in-process with full Node access, the same as a `function` route; add
7
+ `sandbox: true` on the route to run the whole chain isolated instead (see
8
+ [trust model and sandbox opt-in](FUNCTION-SECURITY.md)).
6
9
 
7
10
  ```yaml
8
11
  version: "1"
@@ -28,6 +31,8 @@ Paths resolve from the project root. Up to 16 entries are allowed; each accepts
28
31
  `source` and optional `export` (default `default`). Reuse a module across routes.
29
32
  There are no global middleware settings or middleware-specific argument fields.
30
33
  This is URLCode's small portable API, not Express/Node middleware compatibility.
34
+ Fourteen ready-to-copy patterns (auth, CORS, tracing, error boundary, ETag and
35
+ more) are in [middleware examples](MIDDLEWARE-EXAMPLES.md).
31
36
 
32
37
  ## Order and responses
33
38
 
@@ -47,37 +52,45 @@ reading one consumes it for downstream code. There is no `clone()` or streaming
47
52
  API; pass parsed data through `context.state` when needed.
48
53
 
49
54
  Function responses support the existing text/JSON guest API. To transform their
50
- body, read it and return a new `Response`. Native redirect/respond/link/page/static/
55
+ body, read it and return a new `Response`. Native redirect/respond/page/static/
51
56
  download bodies are opaque and cannot be read through `text()` or `json()`.
52
57
  Returning the same native response preserves original bytes, including binary
53
58
  files, ranges and HEAD lengths. You may add headers, but cannot change its
54
59
  original status or existing native headers while preserving that body. To replace
55
60
  status, destination or content, return a new `Response` instead. Replacement
56
- responses follow the normal sandbox text/JSON and size limits. To wrap a shared
61
+ responses follow the normal response size limits, and (on a `sandbox: true`
62
+ route) the guest's text/JSON constraints. To wrap a shared
57
63
  template around file content, render it through a function at build time and
58
64
  publish the result: see [prerendering](PRERENDER.md).
59
65
 
60
66
  Route selection, enabled/expiry checks, methods and input/body validation run
61
67
  before middleware. Their errors do not pass through the chain. A missing file
62
- inside a selected static mount is a downstream 404 response. Stored-link lookup
63
- errors (missing, disabled, expired or unavailable store) precede middleware. YAML
68
+ inside a selected static mount is a downstream 404 response. YAML
64
69
  `response.headers` apply last and override matching middleware headers. Runtime
65
70
  framing and asset metadata protections still apply.
66
71
 
67
- ## Isolation and testing
72
+ ## Trust, isolation and testing
68
73
 
69
- The whole chain and handler run in one fresh QuickJS/WASM guest with one memory
70
- budget and one deadline. No Node, filesystem, shell, fetch or ambient environment
71
- is exposed. Modules can only access this route's declared dependency graphs.
72
- All middleware receive that route's approved bindings, so review the whole chain;
73
- source changes invalidate grants. See [security](FUNCTION-SECURITY.md).
74
+ The whole chain and handler run as one unit, in one execution mode, chosen by
75
+ the route's `sandbox` field not a per-middleware-entry choice. By default
76
+ (`sandbox` false/absent) that means trusted, in-process execution with full
77
+ Node access and no fixed worker-pool ceiling. With `sandbox: true` it means
78
+ one fresh QuickJS/WASM guest with one memory budget and one deadline: no
79
+ Node, filesystem, shell, fetch or ambient environment, and modules limited to
80
+ this route's declared dependency graph. Either way, all middleware on a route
81
+ receive that route's approved bindings, so review the whole chain; source
82
+ changes invalidate grants. See [trust model and sandbox opt-in](FUNCTION-SECURITY.md).
74
83
 
75
- Invalid responses and repeated `next()` calls fail with 502, exhausted capacity
76
- returns 503, and the shared deadline returns 504. Middleware cannot extend the
77
- deadline or catch the outer worker termination. Forgotten downstream work is
78
- still drained within that deadline.
84
+ Invalid responses and repeated `next()` calls fail with 502, and the deadline
85
+ returns 504 either way. A `sandbox: true` chain also sheds load with 503 when
86
+ the shared worker pool is exhausted, and cannot extend the deadline or catch
87
+ the outer worker termination; forgotten downstream work is still drained
88
+ within it. A trusted chain has no worker pool to exhaust (see
89
+ [capacity](CAPACITY.md)), but its deadline is a race against the call's own
90
+ promise rather than a forced kill — it cannot preempt code that blocks the
91
+ event loop synchronously.
79
92
 
80
93
  Include explicit request fixtures for middleware-wrapped routes: test success,
81
94
  early responses, validation failures and every configured method. Audit will
82
95
  report missing coverage instead of assuming native handler behavior. Benchmark
83
- with middleware enabled to measure its actual sandbox overhead.
96
+ with middleware enabled to measure its actual overhead, sandboxed or trusted.