@jimhoyd/urlcode 0.3.0 → 0.4.0-alpha.1

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 (326) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +106 -0
  2. package/.claude/skills/urlcode-operations/SKILL.md +114 -0
  3. package/.claude-plugin/marketplace.json +18 -0
  4. package/CONTRIBUTING.md +30 -2
  5. package/README.md +157 -230
  6. package/ROADMAP.md +85 -2
  7. package/SECURITY.md +8 -1
  8. package/dist/BUILD-MANIFEST.json +62 -32
  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/bulk.js +37 -0
  17. package/dist/capabilities.js +208 -0
  18. package/dist/capability-query.js +72 -0
  19. package/dist/catalog.js +105 -0
  20. package/dist/cli.js +154 -15
  21. package/dist/client-address.js +1 -1
  22. package/dist/compliance-rules/baseline.js +8 -8
  23. package/dist/compliance-rules/privacy.js +3 -3
  24. package/dist/compliance-rules/strict.js +5 -5
  25. package/dist/conditions.js +88 -0
  26. package/dist/config.js +60 -4
  27. package/dist/context.js +156 -0
  28. package/dist/ecosystem-cli.js +88 -0
  29. package/dist/egress.js +98 -0
  30. package/dist/examples.js +92 -0
  31. package/dist/explain-cli.js +64 -0
  32. package/dist/explain.js +132 -0
  33. package/dist/extensions.js +172 -0
  34. package/dist/function-sources.js +15 -3
  35. package/dist/index.js +37 -0
  36. package/dist/init-with.js +165 -0
  37. package/dist/interchange-cli.js +42 -0
  38. package/dist/interchange.js +189 -0
  39. package/dist/link-cli.js +1 -1
  40. package/dist/management-policy.js +0 -1
  41. package/dist/manifest.js +111 -0
  42. package/dist/match.js +2 -2
  43. package/dist/mcp-authoring.js +147 -0
  44. package/dist/mcp.js +97 -0
  45. package/dist/observability.js +6 -0
  46. package/dist/operator-host.js +29 -0
  47. package/dist/plugins.js +12 -0
  48. package/dist/policies/agents.js +2 -2
  49. package/dist/policies/compression.js +2 -1
  50. package/dist/policies/security.js +0 -0
  51. package/dist/policies.js +1 -1
  52. package/dist/policy.js +29 -7
  53. package/dist/prerender.js +100 -41
  54. package/dist/project-tests.js +3 -3
  55. package/dist/provider-verification.js +92 -0
  56. package/dist/proxy.js +44 -0
  57. package/dist/readiness.js +5 -5
  58. package/dist/recipes.js +41 -0
  59. package/dist/route-diff.js +106 -0
  60. package/dist/router.js +42 -2
  61. package/dist/runtime.js +95 -14
  62. package/dist/schema-query.js +62 -0
  63. package/dist/signals.js +24 -0
  64. package/dist/site.js +0 -0
  65. package/dist/tooling.js +96 -0
  66. package/dist/types/adapters.d.ts +7 -4
  67. package/dist/types/agent-lists.d.ts +0 -1
  68. package/dist/types/agents-guide.d.ts +17 -0
  69. package/dist/types/authoring-files.d.ts +10 -0
  70. package/dist/types/aws.d.ts +3 -1
  71. package/dist/types/build-cloudflare.d.ts +1 -0
  72. package/dist/types/bulk.d.ts +27 -0
  73. package/dist/types/capabilities.d.ts +55 -0
  74. package/dist/types/capability-query.d.ts +24 -0
  75. package/dist/types/catalog.d.ts +65 -0
  76. package/dist/types/client-address.d.ts +0 -1
  77. package/dist/types/compliance-rules/baseline.d.ts +1 -9
  78. package/dist/types/compliance-rules/privacy.d.ts +1 -4
  79. package/dist/types/compliance-rules/strict.d.ts +0 -5
  80. package/dist/types/conditions.d.ts +19 -0
  81. package/dist/types/config.d.ts +20 -2
  82. package/dist/types/context.d.ts +65 -0
  83. package/dist/types/ecosystem-cli.d.ts +17 -0
  84. package/dist/types/egress.d.ts +46 -0
  85. package/dist/types/examples.d.ts +50 -0
  86. package/dist/types/explain-cli.d.ts +11 -0
  87. package/dist/types/explain.d.ts +95 -0
  88. package/dist/types/extensions.d.ts +122 -0
  89. package/dist/types/function-sources.d.ts +5 -0
  90. package/dist/types/index.d.ts +33 -0
  91. package/dist/types/init-with.d.ts +30 -0
  92. package/dist/types/interchange-cli.d.ts +16 -0
  93. package/dist/types/interchange.d.ts +42 -0
  94. package/dist/types/link-cli.d.ts +2 -1
  95. package/dist/types/management-policy.d.ts +0 -1
  96. package/dist/types/manifest.d.ts +81 -0
  97. package/dist/types/match.d.ts +1 -0
  98. package/dist/types/mcp-authoring.d.ts +92 -0
  99. package/dist/types/mcp.d.ts +12 -0
  100. package/dist/types/observability.d.ts +2 -0
  101. package/dist/types/operator-host.d.ts +8 -0
  102. package/dist/types/plugins.d.ts +2 -0
  103. package/dist/types/policies/agents.d.ts +0 -2
  104. package/dist/types/policies/compression.d.ts +2 -0
  105. package/dist/types/policies/security.d.ts +0 -1
  106. package/dist/types/policy.d.ts +15 -4
  107. package/dist/types/project-tests.d.ts +3 -1
  108. package/dist/types/provider-verification.d.ts +53 -0
  109. package/dist/types/proxy.d.ts +21 -0
  110. package/dist/types/readiness.d.ts +1 -1
  111. package/dist/types/recipes.d.ts +30 -0
  112. package/dist/types/route-diff.d.ts +27 -0
  113. package/dist/types/runtime.d.ts +11 -0
  114. package/dist/types/schema-query.d.ts +12 -0
  115. package/dist/types/signals.d.ts +25 -0
  116. package/dist/types/site.d.ts +0 -1
  117. package/dist/types/tooling.d.ts +115 -0
  118. package/dist/types/types.d.ts +57 -0
  119. package/dist/types/typescript-authoring.d.ts +12 -0
  120. package/dist/types/vercel.d.ts +3 -1
  121. package/dist/types/verify-deployment.d.ts +47 -0
  122. package/dist/types.js +21 -2
  123. package/dist/typescript-authoring.js +104 -0
  124. package/dist/vercel.js +4 -3
  125. package/dist/verify-deployment.js +270 -0
  126. package/docs/AI-AUTHORING.md +130 -8
  127. package/docs/BULK.md +79 -0
  128. package/docs/CAPABILITIES.md +179 -0
  129. package/docs/CAPACITY.md +1 -1
  130. package/docs/CI.md +142 -0
  131. package/docs/CONDITIONS.md +74 -0
  132. package/docs/DEPLOYMENT-CHECKS.md +108 -0
  133. package/docs/DYNAMIC-LINKS.md +18 -518
  134. package/docs/EGRESS.md +125 -0
  135. package/docs/EXTENSIONS.md +226 -0
  136. package/docs/FRAMEWORK.md +182 -0
  137. package/docs/INSTALL.md +45 -7
  138. package/docs/INTERCHANGE.md +134 -0
  139. package/docs/MIDDLEWARE-EXAMPLES.md +75 -0
  140. package/docs/MIDDLEWARE.md +2 -0
  141. package/docs/NEXT-PHASE-PLAN.md +90 -0
  142. package/docs/NEXT-STEPS.md +415 -0
  143. package/docs/OBSERVABILITY.md +4 -2
  144. package/docs/OPERATIONAL-PROOF.md +4 -1
  145. package/docs/OPERATIONS.md +6 -3
  146. package/docs/PLUGINS.md +37 -0
  147. package/docs/POLICIES.md +12 -309
  148. package/docs/PRERENDER.md +40 -0
  149. package/docs/PROJECT-DIRECTION.md +42 -0
  150. package/docs/PROVIDER-VERIFICATION.md +84 -0
  151. package/docs/READINESS.md +21 -1
  152. package/docs/README.md +82 -31
  153. package/docs/RECIPES.md +99 -0
  154. package/docs/RELEASE-READINESS.md +11 -9
  155. package/docs/RELEASE-SECURITY.md +27 -4
  156. package/docs/SECURITY-AUDIT.md +1 -1
  157. package/docs/SPECIFICATION.md +95 -8
  158. package/docs/SPIKE-BUSINESS-SUITE.md +1013 -0
  159. package/docs/SPIKE-EXTENSION-MODEL.md +419 -0
  160. package/docs/SPIKE-EXTENSIONS.md +1 -0
  161. package/docs/SPIKE-LAMBDA-COMPILE.md +199 -0
  162. package/docs/STANDARDS.md +150 -142
  163. package/docs/STARTERS.md +21 -1
  164. package/docs/TOOLING.md +291 -0
  165. package/docs/TYPESCRIPT-AUTHORING.md +67 -0
  166. package/docs/TYPESCRIPT.md +1 -1
  167. package/docs/USABILITY-REVIEW.md +123 -0
  168. package/docs/YAML-GUIDE.md +18 -479
  169. package/docs/YAML-REFERENCE.md +127 -16
  170. package/docs/links/cli.md +110 -0
  171. package/docs/links/limits.md +175 -0
  172. package/docs/links/management-api.md +80 -0
  173. package/docs/links/pools.md +75 -0
  174. package/docs/links/setup.md +135 -0
  175. package/docs/policies/agents.md +1 -1
  176. package/docs/policies/contract.md +52 -0
  177. package/docs/policies/hardened.md +56 -0
  178. package/docs/policies/interoperability.md +169 -0
  179. package/docs/policies/operations.md +45 -0
  180. package/docs/yaml/assets.md +36 -0
  181. package/docs/yaml/conditions.md +20 -0
  182. package/docs/yaml/functions.md +160 -0
  183. package/docs/yaml/links.md +30 -0
  184. package/docs/yaml/middleware.md +29 -0
  185. package/docs/yaml/organization.md +74 -0
  186. package/docs/yaml/policies.md +37 -0
  187. package/docs/yaml/redirects.md +64 -0
  188. package/docs/yaml/responses.md +57 -0
  189. package/docs/yaml/site.md +24 -0
  190. package/examples/assets/example.yaml +17 -0
  191. package/examples/aws/example.yaml +20 -0
  192. package/examples/cloudflare/example.yaml +19 -0
  193. package/examples/compliance/example.yaml +11 -0
  194. package/examples/conditions/README.md +12 -0
  195. package/examples/conditions/example.yaml +19 -0
  196. package/examples/conditions/tests/requests.json +13 -0
  197. package/examples/conditions/urlcode.yaml +24 -0
  198. package/examples/cookbook/README.md +8 -4
  199. package/examples/cookbook/example.yaml +17 -0
  200. package/examples/cookbook/functions/catalog.mjs +3 -0
  201. package/examples/cookbook/functions/fail.mjs +4 -0
  202. package/examples/cookbook/functions/items.mjs +3 -0
  203. package/examples/cookbook/functions/profile.mjs +3 -0
  204. package/examples/cookbook/functions/resource.mjs +3 -0
  205. package/examples/cookbook/functions/status.mjs +3 -0
  206. package/examples/cookbook/middleware/auth.mjs +48 -0
  207. package/examples/cookbook/middleware/body.mjs +15 -0
  208. package/examples/cookbook/middleware/bucket.mjs +19 -0
  209. package/examples/cookbook/middleware/cors.mjs +21 -0
  210. package/examples/cookbook/middleware/debug.mjs +13 -0
  211. package/examples/cookbook/middleware/envelope.mjs +11 -0
  212. package/examples/cookbook/middleware/errors.mjs +11 -0
  213. package/examples/cookbook/middleware/etag.mjs +18 -0
  214. package/examples/cookbook/middleware/locale.mjs +16 -0
  215. package/examples/cookbook/middleware/maintenance.mjs +10 -0
  216. package/examples/cookbook/middleware/methods.mjs +15 -0
  217. package/examples/cookbook/middleware/negotiate.mjs +20 -0
  218. package/examples/cookbook/middleware/referer.mjs +12 -0
  219. package/examples/cookbook/middleware/request-id.mjs +16 -0
  220. package/examples/cookbook/route-index.json +676 -0
  221. package/examples/cookbook/routes/middleware.yaml +126 -0
  222. package/examples/cookbook/tests/requests.json +526 -0
  223. package/examples/cookbook/urlcode.yaml +1 -0
  224. package/examples/egress/README.md +22 -0
  225. package/examples/egress/example.yaml +19 -0
  226. package/examples/egress/urlcode.yaml +19 -0
  227. package/examples/extensions/README.md +7 -0
  228. package/examples/extensions/example.yaml +21 -0
  229. package/examples/extensions/urlcode.yaml +25 -0
  230. package/examples/live-links/example.yaml +21 -0
  231. package/examples/monitoring/example.yaml +8 -0
  232. package/examples/prerender/example.yaml +16 -0
  233. package/examples/provider-conformance/README.md +12 -0
  234. package/examples/provider-conformance/example.yaml +14 -0
  235. package/examples/provider-conformance/urlcode.yaml +34 -0
  236. package/examples/tunnel/example.yaml +8 -0
  237. package/examples/vercel/example.yaml +19 -0
  238. package/llms-full.txt +2709 -0
  239. package/llms.txt +48 -19
  240. package/package.json +29 -7
  241. package/packaging/claude-plugin/.claude-plugin/plugin.json +19 -0
  242. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +106 -0
  243. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +114 -0
  244. package/recipes/authenticated-json-api/README.md +51 -0
  245. package/recipes/authenticated-json-api/functions/profile.mjs +5 -0
  246. package/recipes/authenticated-json-api/recipe.yaml +34 -0
  247. package/recipes/authenticated-json-api/tests/requests.json +39 -0
  248. package/recipes/authenticated-json-api/urlcode.yaml +12 -0
  249. package/recipes/contact-form/README.md +25 -0
  250. package/recipes/contact-form/functions/contact.mjs +17 -0
  251. package/recipes/contact-form/recipe.yaml +33 -0
  252. package/recipes/contact-form/tests/requests.json +47 -0
  253. package/recipes/contact-form/urlcode.yaml +18 -0
  254. package/recipes/cors-api/README.md +16 -0
  255. package/recipes/cors-api/functions/items.mjs +3 -0
  256. package/recipes/cors-api/middleware/cors.mjs +21 -0
  257. package/recipes/cors-api/recipe.yaml +26 -0
  258. package/recipes/cors-api/tests/requests.json +65 -0
  259. package/recipes/cors-api/urlcode.yaml +12 -0
  260. package/recipes/health-page/README.md +13 -0
  261. package/recipes/health-page/recipe.yaml +23 -0
  262. package/recipes/health-page/tests/requests.json +36 -0
  263. package/recipes/health-page/urlcode.yaml +19 -0
  264. package/recipes/json-api/README.md +6 -0
  265. package/recipes/json-api/functions/echo.mjs +3 -0
  266. package/recipes/json-api/recipe.yaml +25 -0
  267. package/recipes/json-api/tests/requests.json +34 -0
  268. package/recipes/json-api/urlcode.yaml +12 -0
  269. package/recipes/middleware/README.md +34 -0
  270. package/recipes/middleware/functions/catalog.mjs +3 -0
  271. package/recipes/middleware/functions/fail.mjs +4 -0
  272. package/recipes/middleware/functions/items.mjs +3 -0
  273. package/recipes/middleware/functions/profile.mjs +3 -0
  274. package/recipes/middleware/functions/resource.mjs +3 -0
  275. package/recipes/middleware/functions/status.mjs +3 -0
  276. package/recipes/middleware/middleware/auth.mjs +48 -0
  277. package/recipes/middleware/middleware/body.mjs +15 -0
  278. package/recipes/middleware/middleware/bucket.mjs +19 -0
  279. package/recipes/middleware/middleware/cors.mjs +21 -0
  280. package/recipes/middleware/middleware/debug.mjs +13 -0
  281. package/recipes/middleware/middleware/envelope.mjs +11 -0
  282. package/recipes/middleware/middleware/errors.mjs +11 -0
  283. package/recipes/middleware/middleware/etag.mjs +18 -0
  284. package/recipes/middleware/middleware/locale.mjs +16 -0
  285. package/recipes/middleware/middleware/maintenance.mjs +10 -0
  286. package/recipes/middleware/middleware/methods.mjs +15 -0
  287. package/recipes/middleware/middleware/negotiate.mjs +20 -0
  288. package/recipes/middleware/middleware/referer.mjs +12 -0
  289. package/recipes/middleware/middleware/request-id.mjs +16 -0
  290. package/recipes/middleware/public/guide.txt +1 -0
  291. package/recipes/middleware/recipe.yaml +50 -0
  292. package/recipes/middleware/tests/requests.json +528 -0
  293. package/recipes/middleware/urlcode.yaml +127 -0
  294. package/recipes/protected-download/README.md +22 -0
  295. package/recipes/protected-download/files/report.txt +1 -0
  296. package/recipes/protected-download/recipe.yaml +31 -0
  297. package/recipes/protected-download/tests/requests.json +32 -0
  298. package/recipes/protected-download/urlcode.yaml +15 -0
  299. package/recipes/redirect/README.md +7 -0
  300. package/recipes/redirect/recipe.yaml +25 -0
  301. package/recipes/redirect/tests/requests.json +19 -0
  302. package/recipes/redirect/urlcode.yaml +9 -0
  303. package/recipes/static-plus-api/README.md +15 -0
  304. package/recipes/static-plus-api/functions/info.mjs +3 -0
  305. package/recipes/static-plus-api/public/assets/index.html +3 -0
  306. package/recipes/static-plus-api/public/assets/site.css +1 -0
  307. package/recipes/static-plus-api/public/index.html +8 -0
  308. package/recipes/static-plus-api/recipe.yaml +29 -0
  309. package/recipes/static-plus-api/tests/requests.json +56 -0
  310. package/recipes/static-plus-api/urlcode.yaml +23 -0
  311. package/recipes/typescript/README.md +7 -0
  312. package/recipes/typescript/functions/hello.ts +5 -0
  313. package/recipes/typescript/recipe.yaml +23 -0
  314. package/recipes/typescript/tests/requests.json +18 -0
  315. package/recipes/typescript/urlcode.yaml +5 -0
  316. package/recipes/webhook-receiver/README.md +16 -0
  317. package/recipes/webhook-receiver/functions/receive.mjs +16 -0
  318. package/recipes/webhook-receiver/recipe.yaml +26 -0
  319. package/recipes/webhook-receiver/tests/requests.json +59 -0
  320. package/recipes/webhook-receiver/urlcode.yaml +16 -0
  321. package/schemas/recipe.schema.json +138 -0
  322. package/schemas/urlcode.schema.json +656 -80
  323. package/skills/urlcode/SKILL.md +98 -0
  324. package/starters/default/.github/workflows/urlcode.yml +23 -0
  325. package/starters/default/.mcp.json +12 -0
  326. package/starters/default/AGENTS.md +79 -0
@@ -0,0 +1,135 @@
1
+ # Dynamic links: Setup: requirements, YAML and the example
2
+
3
+ Part of [dynamic links](../DYNAMIC-LINKS.md), which indexes every page.
4
+
5
+ ## Node build requirement
6
+
7
+ Live links need a Node build carrying the patched SQLite WAL fix: SQLite 3.51.3
8
+ or newer, 3.50.7 or 3.44.6. Node bundles SQLite, so this is a property of the
9
+ build, not something you can install separately, and some current releases on a
10
+ supported Node line ship an unpatched version. Run `urlcode doctor` and check
11
+ `liveLinks`; activation fails closed and names the detected version when the
12
+ build is unsuitable. Everything except live links works on any supported Node.
13
+
14
+ Available in URLCode 0.1.0. Define a stable route once; create, update and delete
15
+ short-code records while `serve` keeps running. No YAML rewrite, route snapshot
16
+ rebuild, Git commit or public-server restart is needed for record changes.
17
+
18
+ This first adapter uses optional SQLite storage on one host. Multiple processes
19
+ on that host can share it. No database is required for ordinary YAML-defined
20
+ routes. Network filesystems, multi-host replication and serverless ephemeral disks
21
+ are not supported by this adapter. SQLite WAL requires processes on the same
22
+ host; see [SQLite WAL](https://www.sqlite.org/wal.html).
23
+
24
+ ## Behavior in YAML, data outside Git
25
+
26
+ ```yaml
27
+ version: "1"
28
+ dynamicLinks: true
29
+ routes:
30
+ /r/{code}:
31
+ parameters:
32
+ - name: code
33
+ in: path
34
+ required: true
35
+ schema: {type: string, minLength: 1, maxLength: 128}
36
+ link:
37
+ collection: links
38
+ code: {from: path, name: code}
39
+ ```
40
+
41
+ `link` is a seventh primary handler, exclusive with `function`, `redirect`,
42
+ `respond`, `page`, `static` and `download`. Its `code` is a declared path-input
43
+ reference. `collection` is a logical name, not a filesystem path. GET and HEAD
44
+ are supported; normal route precedence, lifecycle and input validation apply.
45
+ An exact YAML route can intentionally shadow a stored code's URL.
46
+
47
+ The operator binds `links` to a database outside the project. YAML cannot choose
48
+ a database file or grant itself storage. The built-in public binding opens it
49
+ read-only. It performs an indexed database lookup for each request, with no
50
+ application cache or negative cache. Requests starting after an acknowledged
51
+ mutation can see it immediately; an already-running request can finish with an
52
+ older result. Browser/proxy caches remain outside that guarantee.
53
+
54
+ Missing or disabled record: 404. Expired record: 410. Unavailable/overloaded
55
+ store: 503. Invalid code syntax: 404 after route input validation. Redirects
56
+ default to 302 and no-store. Query strings are not forwarded. A record's URL is
57
+ literal HTTP(S), with no embedded credentials, control characters or interpolation.
58
+ It is a redirect, not a server-side fetch. The store does not verify destination
59
+ ownership, safety or availability; application abuse prevention remains necessary.
60
+
61
+ ## Run the included example
62
+
63
+ From the runtime checkout, choose a private data directory **outside the app**.
64
+ Its parent directory must already exist. The following POSIX-shell example uses
65
+ an operator-created directory; on Windows use an absolute local drive path.
66
+
67
+ ```sh
68
+ mkdir -p ../urlcode-data
69
+ URLCODE_DATA="$(cd ../urlcode-data && pwd)"
70
+ node src/cli.ts links init --store "$URLCODE_DATA/links.sqlite"
71
+ node src/cli.ts links create --store "$URLCODE_DATA/links.sqlite" \
72
+ --code demo --destination https://example.com/demo
73
+ node src/cli.ts serve --project examples/live-links \
74
+ --link-store "links=$URLCODE_DATA/links.sqlite" --port 3000
75
+ ```
76
+
77
+ Open `/r/demo`. In a second terminal, run another `links create` with an unused
78
+ code against the same absolute file; its URL works immediately. Omit `--code`
79
+ to generate a cryptographically random 16-character base64url code. Codes allow
80
+ 1–128 letters, digits, underscores and hyphens; collection names begin with a
81
+ letter and contain at most 64 letters/digits/underscores/hyphens.
82
+
83
+ The CLI defaults to collection `links`; specify `--collection` to manage another.
84
+ Serving currently accepts one `--link-store collection=/absolute/file` binding;
85
+ embedded applications may supply multiple adapters. Commands do not copy records
86
+ into YAML or create route definitions automatically.
87
+
88
+ ## Explicit project opt-in
89
+
90
+ Only the entry `urlcode.yaml` may set `dynamicLinks: true`. It defaults to false;
91
+ the starter writes `dynamicLinks: false` explicitly. Included route files cannot
92
+ set or override it. Any `link` handler, including a disabled route, or runtime
93
+ link-store binding requires the opt-in. This flag means live stored-link records,
94
+ not parameterized redirects, custom functions, middleware or development reload.
95
+
96
+ `validate`, `routes`, `audit` and `scaffold` reports expose `dynamicLinks` as a
97
+ boolean. Enabling it grants no storage access to guest code and starts no
98
+ management endpoint. The operator still supplies the external store binding;
99
+ management remains a separate authenticated service. Standalone `links` CRUD/API
100
+ commands operate the operator's store independently of this public-runtime flag.
101
+
102
+ Migration: existing live-link projects must add `dynamicLinks: true` to their
103
+ entry file. Refresh revision-pinned function policies for those projects using
104
+ the normal operator review flow; enabling the capability changes the approval
105
+ digest. Projects that omit it or explicitly set false retain their prior digest.
106
+ To disable, remove `link` declarations and serving-store bindings, then set false
107
+ and validate/redeploy. Editing the flag alone does not stop an already running
108
+ production process or management API. Invalid reloads retain the last-good state.
109
+
110
+ ## Middleware, sandbox and tests
111
+
112
+ Middleware may wrap a successful link response; the usual native metadata/body
113
+ rules apply. Lookup/missing/disabled/expiry errors happen before middleware. The
114
+ handler does not make database objects available to guest code. Functions still
115
+ have no filesystem, SQL, fetch, storage broker or management token capability.
116
+ Link changes do not alter configuration/source digests or invalidate unrelated
117
+ function-binding grants. Changing YAML still does.
118
+
119
+ `routes` and `audit --expect-routes` count definitions, not stored records. Link
120
+ routes require explicit successful GET/HEAD fixtures; there is no assumed fixture
121
+ for live data. Seed a disposable test database, then pass `--link-store` to
122
+ validate/test/audit/benchmark. Do not run mutation tests against production.
123
+
124
+ ```sh
125
+ node src/cli.ts test --project examples/live-links --link-store links=/absolute/test-links.sqlite
126
+ node src/cli.ts audit --project examples/live-links --link-store links=/absolute/test-links.sqlite --expect-routes 2
127
+ ```
128
+
129
+ The example expects `demo -> https://example.com/demo` and an unused `not-created`
130
+ code. Tests cover create/update/delete visibility without reload, persistence,
131
+ concurrent conflicts, expiry/disabled semantics, scope, public/admin separation,
132
+ body/token boundaries, overload and acknowledged writes after abrupt writer exit.
133
+ Production durability, sustained load and recovery drills still require validation
134
+ on your actual storage. General application state, WebRTC sessions, user-account
135
+ APIs and arbitrary runtime code registration remain separate future work.
@@ -91,7 +91,7 @@ deploy carries a known list version and a rollback rolls the list back too.
91
91
  ### Refreshing
92
92
 
93
93
  ```sh
94
- node scripts/sync-agent-lists.ts # fetch pinned upstreams, validate, write data/agents/
94
+ node scripts/sync-agent-lists.ts # fetch pinned upstreams, validate, write data/agents/ (also: npm run sync:agents)
95
95
  node scripts/sync-agent-lists.ts --check # exit 1 when the committed files are stale
96
96
  ```
97
97
 
@@ -0,0 +1,52 @@
1
+ # Policies: The policy contract and your own patterns
2
+
3
+ Part of [policies](../POLICIES.md), which holds the shared rules and the per-target table.
4
+
5
+ ## The policy contract in TypeScript
6
+
7
+ The policies are modules of one shape, `PolicyModule<Config, State>` in
8
+ `src/types.ts`: `targets`, `compile`, `onRequest`, `onResponse`, optional
9
+ `onError`, `describe` and `close`. `@jimhoyd/urlcode/policies` exports that type with
10
+ `PolicyRequest`, `PolicyContext`, `PolicyChain`, `PolicyShared` and
11
+ `PolicyRegistry` (the five built-ins keyed by name), and the declarations ship
12
+ with the package. A per-policy configuration is typed as the YAML it accepts,
13
+ and a `profiles` layer may hold a partial one:
14
+
15
+ ```ts
16
+ import { registry, targets, type PolicyRegistry, type PolicyRequest } from '@jimhoyd/urlcode/policies';
17
+
18
+ const throttle: PolicyRegistry['throttle'] = registry.throttle; // PolicyModule<ThrottleConfig, ThrottleState>
19
+ const support = throttle.targets({ quota: 120, window: 60, partition: 'client', status: 429 }); // per-target support for this config
20
+ console.log(support.vercel, support.cloudflare);
21
+ function inspect(request: PolicyRequest): void { console.log(request.route, request.client, targets); }
22
+ ```
23
+
24
+ The registry is read-only: a project cannot add a policy from YAML, and an
25
+ operator adds behavior through [plugins](../PLUGINS.md), not by editing it.
26
+
27
+ ## Supplying your own patterns
28
+
29
+ The runtime ships mechanisms and one named profile, not an opinion about who
30
+ should be blocked. Ways to express your own:
31
+
32
+ - **Custom profiles.** Define any number under `profiles` and select one per
33
+ project or per route. Profiles are plain data and travel with the YAML.
34
+ - **Per-route overrides.** Any key can be tightened, replaced or set to
35
+ `false` on a route.
36
+ - **Own agent lists.** `agents.deny` and `agents.allow` accept bundled list
37
+ names and project-relative `.json` files in the same schema, so a list you
38
+ do not want to redistribute stays yours. `denyPatterns`/`allowPatterns`
39
+ take a bounded, linear-time pattern subset. See [agents](agents.md).
40
+ - **Header by header.** `security.set` adds or overrides a header and wins over
41
+ the profile, YAML `response.headers` and handler output; `security.unset`
42
+ drops one the profile would emit. Headers the runtime or a handler owns
43
+ (`content-type`, `cache-control`, `set-cookie`, `etag`, `location`, and the
44
+ rest listed in `src/policies/security.ts`) cannot be `set`.
45
+ See [security](security.md).
46
+ - **Explicit cache fields.** A strategy sets defaults; `maxAge`,
47
+ `staleWhileRevalidate`, `staleIfError`, `cdnMaxAge`, `originTtl`, `vary`,
48
+ `statuses`, `maxBytes` and `maxEntries` override what it implies.
49
+ See [cache](cache.md).
50
+ - **Plugins.** Verified-bot checks, shared-store throttling, purge endpoints
51
+ and anything vendor-specific are host code an operator passes in;
52
+ see [plugins](../PLUGINS.md).
@@ -0,0 +1,56 @@
1
+ # Policies: The built-in `hardened` profile and hardening guidance
2
+
3
+ Part of [policies](../POLICIES.md), which holds the shared rules and the per-target table.
4
+
5
+ ## The built-in `hardened` profile
6
+
7
+ `policies.profile: hardened` expands to the following and nothing else, so it
8
+ can be read in one place and overridden key by key. This is
9
+ `builtinProfiles.hardened` in `src/policies.ts`:
10
+
11
+ ```yaml
12
+ policies:
13
+ security: { headers: oshp }
14
+ agents: { deny: [ai-crawlers], status: 403 }
15
+ throttle: { quota: 120, window: 60, partition: client, status: 429 }
16
+ compression: { encodings: [br, gzip], minBytes: 1024 }
17
+ cache: { strategy: revalidate }
18
+ ```
19
+
20
+ The numbers are starting points chosen to be safe for a single small instance;
21
+ they are not tuned for any workload and not a security assessment of your
22
+ deployment. Note what the profile implies per target: on Vercel and AWS it
23
+ refuses activation as written, because `throttle.partition: client` is
24
+ refused there; set `throttle: false` (or `partition: route`) at the project
25
+ or route level. `compression` is delegated to the platform. On Cloudflare
26
+ only `agents` and `security` survive and `compression` is delegated, so the
27
+ profile must also drop `throttle` and `cache` there.
28
+ There is no `strict` profile: anything stricter is a per-project decision.
29
+
30
+ ## Hardened configuration guidance
31
+
32
+ Advice, not defaults, condensed from the spike's section 6.
33
+
34
+ 1. **Network and edge first.** Volumetric protection, TLS termination and
35
+ per-client connection budgets stay with the provider or the reverse proxy.
36
+ Runtime policies are a second layer, never the first.
37
+ 2. **Ingress to origin.** Bind privately; allow only the proxy's addresses;
38
+ pass `--trusted-proxies` so `client` partitioning sees the real peer.
39
+ 3. **Request policies.** Agents before throttle: denials are cheaper than
40
+ counting. Start throttle in `mode: report` for a release to see real
41
+ quotas in the headers and logs, then switch to `enforce`.
42
+ 4. **Allow before deny.** Keep an explicit allow for the crawlers you need
43
+ indexed; a broad deny without one is the common self-inflicted outage.
44
+ 5. **Route contract.** Exact methods, `request.body` limits and `expires` on
45
+ campaign routes still do most of the work.
46
+ 6. **Response policies.** Security headers on every route; compression only
47
+ on listed types and never on secret-bearing responses (the BREACH class of
48
+ attack, which is why compression is skipped where a route declares secrets
49
+ unless `allowWithSecrets` says otherwise); caching only with a strategy
50
+ whose semantics you can state, `immutable` only on content-hashed paths,
51
+ `no-store` everywhere else.
52
+ 7. **Lists as pinned data.** Bundled agent lists ship with the release, so a
53
+ rollback rolls the list back too.
54
+ 8. **Read the table.** Check `urlcode routes` on each target you deploy to;
55
+ the same YAML is refused where it cannot be enforced, and that is the
56
+ point.
@@ -0,0 +1,169 @@
1
+ # Policies: Interoperability between policies
2
+
3
+ Part of [policies](../POLICIES.md), which holds the shared rules and the per-target table.
4
+
5
+ ## Interoperability
6
+
7
+ Every pair of policies, and every policy against the runtime's own responses,
8
+ was reviewed by reading the code and by exercising a server. The rules below
9
+ are the ones the current code enforces; where a combination misbehaves, the
10
+ rule says so and names what to do instead.
11
+
12
+ ### Cannot coexist
13
+
14
+ Hard conflicts: the runtime refuses activation, or the combination produces
15
+ a wrong answer today.
16
+
17
+ - **Origin cache and conditional or range requests.** A request carrying
18
+ `If-None-Match`, `If-Modified-Since`, `If-Match`, `If-Unmodified-Since` or
19
+ `Range` is never answered from the origin cache: a stored entry is a full
20
+ `200` representation and the handler owns validators and ranges, so such
21
+ requests always reach it and get the handler's `304`, `412` or `206`.
22
+ Unconditional requests are served from the entry.
23
+ - **`security.unset` and a route that switches profile.** `unset` is
24
+ validated against the profile in effect on that route. A project-level
25
+ `unset` merges into a route that names a profile without that header
26
+ (`headers: off`, or `oshp-no-csp` with `unset: [Content-Security-Policy]`)
27
+ and activation fails with `unset names "...", which the off profile does
28
+ not emit`. Write `unset: []` on that route: a route key replaces the whole
29
+ array.
30
+ - **`security.set` of `Cache-Control`, `Content-Encoding`, `ETag`,
31
+ `Content-Type`, `Set-Cookie`, `Location` and the other reserved names** is
32
+ refused at activation (`is owned by the runtime or handler`). `Vary`,
33
+ `RateLimit`, `RateLimit-Policy`, `Retry-After` and `Age` are reserved for
34
+ the same reason: the cache and throttle policies own them, and a `set` of
35
+ any of the five is refused at activation with the route named. Declare
36
+ `vary` on the cache policy instead.
37
+ - **`agents.denyEmpty`, `throttle` and the generated probes.** `urlcode
38
+ audit`, `urlcode test` and `urlcode benchmark` send generated probes as
39
+ `Mozilla/5.0 (compatible; RouteProbe/0.1)`,
40
+ so `denyEmpty` does not fail them; a deny pattern that matches that string
41
+ would. A tight `throttle` fails an audit once the probes exceed `quota`
42
+ (the audit runs from one address, so a `static` tree with more files than
43
+ the quota trips it): keep the quota at least the number of generated cases
44
+ or run the audit in `mode: report`.
45
+ - **`hardened` on Vercel and AWS** is refused as written, because
46
+ `throttle.partition: client` cannot be honoured there; **on Cloudflare**
47
+ the build refuses `throttle` and `cache`. Override the offending keys with
48
+ `false` or `partition: route` as [the `hardened` page](hardened.md) describes.
49
+ - **`compression.encodings: [zstd]` on a Node without `zlib.zstdCompressSync`**
50
+ fails activation with the route named rather than serving identity.
51
+
52
+ ### Coexist with defined precedence
53
+
54
+ Pairs that work, with the rule the code applies.
55
+
56
+ - **Request order is `agents`, `throttle`, cache lookup.** A denied agent is
57
+ never counted and never looked up; a throttled request is never looked up.
58
+ An agent on an `allow` list passes `agents` and is still throttled. An
59
+ `agents` denial and a `throttle` refusal both pass through the cache
60
+ policy's response hook (they carry no flight, so nothing is stored) and
61
+ pick up its declared `vary` names, then security headers, then
62
+ `Vary: Accept-Encoding` from compression; their bodies are below `minBytes`
63
+ so they are never encoded.
64
+ - **Cache hit and the response phase.** A hit skips only the cache's own
65
+ response hook: it still carries the client's `RateLimit` headers (the
66
+ request was counted), the security profile, and is compressed on the way
67
+ out. Stored entries are the handler's bytes and headers after YAML
68
+ `response.headers` and the cache policy ran, before throttle, security and
69
+ compression: bodies are stored uncompressed and encoded again on every hit,
70
+ and a `RateLimit` value is never stored. A cache hit on an asset serves the
71
+ same snapshot buffer, so a precompressed variant is used for `GET` and
72
+ reported at the variant's length for `HEAD`, exactly as when uncached.
73
+ - **Cache `Vary` and compression `Vary`.** The cache merges its declared
74
+ `vary` names first; compression appends `Accept-Encoding` without
75
+ duplicating it and leaves a `Vary: *` alone. The origin key uses only the
76
+ cache's names: because bodies are stored uncompressed, `Accept-Encoding` is
77
+ not part of the key and need not be.
78
+ - **ETags across `cache` and `compression`.** `revalidate` keeps a handler or
79
+ asset `ETag` and computes a strong SHA-256 tag for a `200` without one.
80
+ Compression then weakens a dynamically encoded body's tag (`W/"..."`) and
81
+ suffixes a precompressed asset's (`"...-gz"`). Revalidation works in every
82
+ combination: the cache's `304` compares weakly, the asset handler compares
83
+ weakly against the identity tag, and compression answers `304` for a
84
+ suffixed tag it produced. The `304` that `revalidate` produces keeps
85
+ `Content-Type`, so compression adds `Vary: Accept-Encoding` to it as it
86
+ does to the asset handler's own `304`.
87
+ - **Who owns `Cache-Control`.** YAML `response.headers` first, then an asset
88
+ handler's `cacheControl` when the cache policy is only inherited, then a
89
+ handler's `private` or `no-store`, then the strategy; `security.set` cannot
90
+ name it. A `no-store` or `private` handler answer is never stored whatever
91
+ the strategy says.
92
+ - **Security headers and everything else.** Profile headers fill gaps only:
93
+ YAML `response.headers`, function, asset, redirect and early-denial headers
94
+ keep their values. `set` overrides all of them. `Strict-Transport-Security`
95
+ needs an `https` origin. The profile is applied on cache hits and on early
96
+ denials (including a plugin short-circuit), on the self-hosted server and
97
+ in the Worker alike.
98
+ - **Compression and secrets or cookies.** A route with `secrets` or a
99
+ response with `Set-Cookie` is sent as identity (still with `Vary`) unless
100
+ `allowWithSecrets: true`. A route with `secrets` also never enters the
101
+ origin cache, so the two policies agree on what a secret-bearing route is.
102
+ - **Throttle and the 405.** The request phase runs before the method check,
103
+ so a request that ends as `405` was counted, and the `405` passes through
104
+ the response phase: it carries the `RateLimit` headers, the security
105
+ profile and plugin `onResponse` rewrites. Nothing stores it.
106
+ - **Plugins and policies.** `onRequest` runs before every policy; a
107
+ short-circuit result skips `agents`, `throttle` and the cache lookup
108
+ (nothing is counted, matched or stored) and skips the cache store and the
109
+ throttle headers, then receives security headers and compression, then
110
+ every plugin's `onResponse`. On a thrown error the policies' `onError`
111
+ hooks run first (in the order the policies were declared) and then plugin
112
+ `onError` hooks in reverse. Only the cache policy has an error hook today
113
+ and it never returns a fallback (`stale-if-error` is header-only); if a
114
+ future policy did, that fallback would pass through the response phase and
115
+ every plugin `onResponse`, and plugin `onError` would not run for that
116
+ request.
117
+ - **Reload.** Every reload builds a new runtime with fresh shared state:
118
+ throttle counters start empty and the origin cache and precompressed
119
+ variants are rebuilt; a client mid-window gets a fresh budget. The plugin
120
+ objects are the ones the operator passed and persist across reloads:
121
+ `onActivate` runs for the new runtime before `onClose` runs for the old,
122
+ so a plugin's own state (a shared-store connection, a `WeakMap`) survives a
123
+ reload unless its `onClose` discards it. A plugin whose `onActivate`
124
+ throws makes the reload fail and the old runtime keeps serving.
125
+ - **Merge edge cases.** A route `profile` that lacks a key the project layer
126
+ had leaves that key in force (layers merge, they do not replace); `false`
127
+ at the project level followed by an object at the route level re-enables
128
+ the policy with the route's object alone, so it must be complete
129
+ (`throttle` needs `quota` and `window`); a custom profile named like a
130
+ built-in shadows it everywhere.
131
+ - **Memory bounds add up.** Per runtime: 64 MiB of asset snapshot, up to
132
+ 64 MiB of precompressed variants, up to 64 MiB of origin-cache bodies (a
133
+ stored asset references the snapshot buffer rather than copying it, but
134
+ is counted against the cache budget), and the throttle table at the
135
+ largest `maxKeys` (100,000 keys by default). Two routes serving the same
136
+ file compress it separately and both count. During a reload both
137
+ generations exist at once. [Capacity](../CAPACITY.md) states the asset,
138
+ throttle and cache figures; the precompressed budget belongs in that table
139
+ too.
140
+ - **Cloudflare.** The artifact carries the effective `agents` and `security`
141
+ configuration per route, with the entries of any project list file
142
+ embedded under the reference as written in YAML (no filesystem path); a
143
+ route-level `agents: false` leaves the artifact without that key. The
144
+ Worker runs `agents` on the request and `security` on the response, so an
145
+ early denial carries the profile exactly as on the self-hosted server.
146
+
147
+ ### Not covered by policies
148
+
149
+ - **Every error the runtime throws** (404 for no match, disabled route or
150
+ missing link, 410, 400/413/415 from body checks, 502/503/504 from the
151
+ sandbox or a link store) bypasses the response phase: no `Vary`, no
152
+ `RateLimit`, no compression, no plugin `onResponse`, on every target.
153
+ What they do get is the `security` policy: the matched route's effective
154
+ profile when the error came after routing (so a route with
155
+ `security: false` answers its 410 bare), otherwise the project-level
156
+ profile, including a host-side error such as an oversized body or shed
157
+ admission and the Worker's own 404. The runtime's fixed headers
158
+ (`Content-Type`, `Cache-Control: no-store`, `Content-Length`,
159
+ `X-Request-Id`, `X-Content-Type-Options`) can never be replaced by it.
160
+ Plugins keep `onError` for observation; a policy error hook may answer with
161
+ a fallback, and none does today.
162
+ - **The audit's probes** share one address and one `User-Agent`, so they do
163
+ not exercise `agents` or `throttle` the way real traffic does;
164
+ the policy table in `testPlan().policies` is the audit's evidence for
165
+ those two.
166
+ - **`stale-if-error` at the origin**: header-only, as the
167
+ [cache page](cache.md) states.
168
+ - **Cross-instance state**: counters and the cache are per runtime on every
169
+ target; a shared budget or a shared cache is a plugin.
@@ -0,0 +1,45 @@
1
+ # Policies: Client identity, inventory and logging
2
+
3
+ Part of [policies](../POLICIES.md), which holds the shared rules and the per-target table.
4
+
5
+ ## Client identity and `--trusted-proxies`
6
+
7
+ `throttle` partitions by `client`. On the self-hosted server the client is the
8
+ socket peer unless `urlcode serve --trusted-proxies 10.0.0.0/8,fd00::/8`
9
+ names the addresses allowed to speak for a client. Then `X-Forwarded-For` is
10
+ walked from the right, skipping trusted hops, and the first untrusted address is
11
+ the client; a chain made only of trusted proxies yields its leftmost entry, and
12
+ a malformed entry is skipped. A forwarded header from a peer
13
+ outside the trusted set is ignored, as is a request carrying more than one
14
+ `X-Forwarded-For` field. Ranges are IPv4 or IPv6 CIDRs (at most 256);
15
+ IPv4-mapped IPv6 peers match IPv4 ranges. `startServer({ trustedProxies })`
16
+ takes the same list.
17
+
18
+ A request whose client cannot be resolved (an adapter without a peer, an
19
+ embedding caller that passes none) shares one bucket rather than being exempt,
20
+ so a misconfigured proxy fails closed. The throttle summary in
21
+ `testPlan().policies` records this as `unresolvedClient: "shared key"`. The runtime still
22
+ never trusts forwarded headers for its public origin; set `--origin`
23
+ explicitly, as [resilience](../RESILIENCE.md) already requires.
24
+
25
+ ## What `routes` and `audit` report
26
+
27
+ `urlcode routes` prints the inventory with a `policies` array per route naming
28
+ the policies effective on it (`testPlan().inventory[].policies`) and the full
29
+ `policies` map. The embedding API and a plugin's `onActivate` see
30
+ `testPlan().policies`, a map from route pattern to each policy's summary with
31
+ its `target` value (`native`, `compiled` or `delegated`), the per-route
32
+ capability table the portability rule calls for. `urlcode audit` prints the
33
+ same table under `policies` and, with `--compliance`, checks the declared
34
+ configuration against standards-referenced rules; see
35
+ [compliance](../COMPLIANCE.md). `urlcode doctor` lists the policy names this
36
+ runtime knows.
37
+
38
+ ## Logging
39
+
40
+ Policies log through the runtime's request log with one-line events:
41
+ `{ event: 'throttle', route, outcome: 'exceeded' | 'allowed', remaining }`,
42
+ `{ event: 'agents', route, list, outcome: 'denied' | 'reported' }` and the
43
+ cache events described on the [cache page](cache.md). Events name the
44
+ configured route pattern and the list or strategy, never a client address, a
45
+ User-Agent string or request text. A logging failure never changes a response.
@@ -0,0 +1,36 @@
1
+ # YAML guide: Pages, static folders and downloads
2
+
3
+ Snippets are entries under `routes:` unless stated otherwise; the [guide index](../YAML-GUIDE.md) lists every page.
4
+
5
+ ## 10. Pages, static folders, downloads and MIME
6
+
7
+ ```yaml
8
+ /about:
9
+ page:
10
+ file: public/about.html
11
+ cacheControl: no-cache
12
+ /assets/*:
13
+ static:
14
+ directory: public/assets
15
+ index: index.html
16
+ cacheControl: public, max-age=3600
17
+ /download:
18
+ download:
19
+ file: public/guide.txt
20
+ filename: urlcode-guide.txt
21
+ contentType: text/plain
22
+ cacheControl: no-store
23
+ ```
24
+
25
+ All files must exist. MIME is detected by extension, not content sniffing; unknown
26
+ extensions become application/octet-stream. `contentType` overrides detection
27
+ without MIME parameters. An override on a static mount affects all its files.
28
+ The download name defaults to the source basename. `index` is opt-in and only
29
+ applies to slash-terminated requests. No automatic slash redirect or SPA fallback.
30
+
31
+ Cache choices: `no-cache` (asset default), `no-store`, `public, max-age=3600`,
32
+ `public, max-age=31536000, immutable`. Reserve immutable caching for versioned
33
+ URLs. GET/HEAD, ETag/date validation and single byte ranges are supported.
34
+ Files stay snapshotted until reload/restart. See [assets](../ASSETS.md) for complete
35
+ conditional/range semantics and publication safety. Files are limited to 16 MiB
36
+ each and 64 MiB total unique bytes per snapshot.
@@ -0,0 +1,20 @@
1
+ # YAML guide: Enable, disable and expire
2
+
3
+ Snippets are entries under `routes:` unless stated otherwise; the [guide index](../YAML-GUIDE.md) lists every page.
4
+
5
+ ## 11. Enable, disable and expire
6
+
7
+ ```yaml
8
+ /paused:
9
+ enabled: false
10
+ redirect: {url: 'https://example.com/'}
11
+ /campaign:
12
+ description: A scheduled end, no scheduled start
13
+ expires: "2030-01-01T00:00:00Z"
14
+ redirect: {url: 'https://example.com/'}
15
+ ```
16
+
17
+ Quote timestamps so they remain strings. Disabled routes return 404; expired
18
+ routes return 410. Expiry is an absolute UTC timestamp, not a TTL. There is no
19
+ start-time scheduler. `description` is authoring metadata. Changing YAML activates
20
+ through dev reload or production restart/deployment; it is not an HTTP mutation.