@jimhoyd/urlcode 0.3.0

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 (254) hide show
  1. package/CONTRIBUTING.md +80 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +20 -0
  4. package/README.md +342 -0
  5. package/ROADMAP.md +248 -0
  6. package/SECURITY.md +40 -0
  7. package/data/agents/LICENSES/ai-robots-txt.txt +24 -0
  8. package/data/agents/LICENSES/crawler-user-agents.txt +23 -0
  9. package/data/agents/ai-crawlers.json +1240 -0
  10. package/data/agents/crawlers.json +10515 -0
  11. package/data/agents/index.d.ts +3 -0
  12. package/data/agents/index.js +2453 -0
  13. package/data/agents/monitoring.json +1520 -0
  14. package/data/agents/seo.json +3865 -0
  15. package/dist/BUILD-MANIFEST.json +60 -0
  16. package/dist/adapters.js +59 -0
  17. package/dist/agent-lists.js +59 -0
  18. package/dist/assets.js +129 -0
  19. package/dist/authoring.js +68 -0
  20. package/dist/aws.js +110 -0
  21. package/dist/build-cloudflare.js +164 -0
  22. package/dist/cli.js +185 -0
  23. package/dist/client-address.js +85 -0
  24. package/dist/cloudflare.js +165 -0
  25. package/dist/compliance-rules/baseline.js +98 -0
  26. package/dist/compliance-rules/privacy.js +38 -0
  27. package/dist/compliance-rules/shared.js +62 -0
  28. package/dist/compliance-rules/strict.js +61 -0
  29. package/dist/compliance.js +219 -0
  30. package/dist/config-worker.js +9 -0
  31. package/dist/config.js +172 -0
  32. package/dist/errors.js +8 -0
  33. package/dist/function-sources.js +54 -0
  34. package/dist/function-worker.js +105 -0
  35. package/dist/functions.js +174 -0
  36. package/dist/guest-api.js +114 -0
  37. package/dist/header-validation.js +18 -0
  38. package/dist/http-policy.js +74 -0
  39. package/dist/http-response.js +84 -0
  40. package/dist/index.js +11 -0
  41. package/dist/link-api.js +136 -0
  42. package/dist/link-cli.js +141 -0
  43. package/dist/link-events.js +76 -0
  44. package/dist/link-records.js +31 -0
  45. package/dist/link-store-worker.js +150 -0
  46. package/dist/link-store.js +250 -0
  47. package/dist/logging.js +22 -0
  48. package/dist/management-policy.js +41 -0
  49. package/dist/match.js +124 -0
  50. package/dist/observability.js +242 -0
  51. package/dist/plugins.js +74 -0
  52. package/dist/policies/agents.js +248 -0
  53. package/dist/policies/cache.js +297 -0
  54. package/dist/policies/compression.js +187 -0
  55. package/dist/policies/security.js +0 -0
  56. package/dist/policies/throttle.js +131 -0
  57. package/dist/policies.js +142 -0
  58. package/dist/policy.js +55 -0
  59. package/dist/prerender.js +165 -0
  60. package/dist/project-tests.js +23 -0
  61. package/dist/readiness.js +216 -0
  62. package/dist/router.js +166 -0
  63. package/dist/runtime.js +244 -0
  64. package/dist/scaffold.js +0 -0
  65. package/dist/scripts/operational-drills.js +75 -0
  66. package/dist/server.js +270 -0
  67. package/dist/site.js +0 -0
  68. package/dist/sqlite-version.js +6 -0
  69. package/dist/types/adapters.d.ts +12 -0
  70. package/dist/types/agent-lists.d.ts +5 -0
  71. package/dist/types/assets.d.ts +13 -0
  72. package/dist/types/authoring.d.ts +2 -0
  73. package/dist/types/aws.d.ts +35 -0
  74. package/dist/types/build-cloudflare.d.ts +14 -0
  75. package/dist/types/cli.d.ts +2 -0
  76. package/dist/types/client-address.d.ts +9 -0
  77. package/dist/types/cloudflare.d.ts +75 -0
  78. package/dist/types/compliance-rules/baseline.d.ts +12 -0
  79. package/dist/types/compliance-rules/privacy.d.ts +6 -0
  80. package/dist/types/compliance-rules/shared.d.ts +24 -0
  81. package/dist/types/compliance-rules/strict.d.ts +9 -0
  82. package/dist/types/compliance.d.ts +119 -0
  83. package/dist/types/config-worker.d.ts +1 -0
  84. package/dist/types/config.d.ts +21 -0
  85. package/dist/types/errors.d.ts +7 -0
  86. package/dist/types/function-sources.d.ts +21 -0
  87. package/dist/types/function-worker.d.ts +1 -0
  88. package/dist/types/functions.d.ts +92 -0
  89. package/dist/types/guest-api.d.ts +16 -0
  90. package/dist/types/header-validation.d.ts +2 -0
  91. package/dist/types/http-policy.d.ts +39 -0
  92. package/dist/types/http-response.d.ts +43 -0
  93. package/dist/types/index.d.ts +11 -0
  94. package/dist/types/link-api.d.ts +30 -0
  95. package/dist/types/link-cli.d.ts +36 -0
  96. package/dist/types/link-events.d.ts +27 -0
  97. package/dist/types/link-records.d.ts +11 -0
  98. package/dist/types/link-store-worker.d.ts +1 -0
  99. package/dist/types/link-store.d.ts +130 -0
  100. package/dist/types/logging.d.ts +9 -0
  101. package/dist/types/management-policy.d.ts +9 -0
  102. package/dist/types/match.d.ts +81 -0
  103. package/dist/types/observability.d.ts +104 -0
  104. package/dist/types/plugins.d.ts +27 -0
  105. package/dist/types/policies/agents.d.ts +86 -0
  106. package/dist/types/policies/cache.d.ts +82 -0
  107. package/dist/types/policies/compression.d.ts +41 -0
  108. package/dist/types/policies/security.d.ts +37 -0
  109. package/dist/types/policies/throttle.d.ts +55 -0
  110. package/dist/types/policies.d.ts +36 -0
  111. package/dist/types/policy.d.ts +21 -0
  112. package/dist/types/prerender.d.ts +31 -0
  113. package/dist/types/project-tests.d.ts +13 -0
  114. package/dist/types/readiness.d.ts +121 -0
  115. package/dist/types/router.d.ts +11 -0
  116. package/dist/types/runtime.d.ts +91 -0
  117. package/dist/types/scaffold.d.ts +17 -0
  118. package/dist/types/server.d.ts +37 -0
  119. package/dist/types/site.d.ts +12 -0
  120. package/dist/types/sqlite-version.d.ts +1 -0
  121. package/dist/types/types.d.ts +332 -0
  122. package/dist/types/vercel.d.ts +12 -0
  123. package/dist/types.js +177 -0
  124. package/dist/vercel.js +65 -0
  125. package/docs/AI-AUTHORING.md +113 -0
  126. package/docs/ASSETS.md +106 -0
  127. package/docs/AWS.md +77 -0
  128. package/docs/BEST-PRACTICES.md +267 -0
  129. package/docs/CAPACITY.md +208 -0
  130. package/docs/CLOUDFLARE.md +110 -0
  131. package/docs/COMPLIANCE.md +242 -0
  132. package/docs/DYNAMIC-LINKS.md +561 -0
  133. package/docs/FUNCTION-SECURITY.md +113 -0
  134. package/docs/HTTP.md +129 -0
  135. package/docs/INSTALL.md +90 -0
  136. package/docs/LOAD-TESTING.md +91 -0
  137. package/docs/LOCAL-DEVELOPMENT.md +99 -0
  138. package/docs/MANAGEMENT-SECURITY.md +82 -0
  139. package/docs/MIDDLEWARE.md +83 -0
  140. package/docs/MONITORING.md +132 -0
  141. package/docs/OBSERVABILITY.md +229 -0
  142. package/docs/OPERATIONAL-PROOF.md +42 -0
  143. package/docs/OPERATIONS.md +207 -0
  144. package/docs/ORGANIZATION.md +135 -0
  145. package/docs/PERFORMANCE.md +72 -0
  146. package/docs/PLUGINS.md +234 -0
  147. package/docs/POLICIES.md +489 -0
  148. package/docs/PRERENDER.md +193 -0
  149. package/docs/PROJECT-DIRECTION.md +51 -0
  150. package/docs/READINESS.md +125 -0
  151. package/docs/README.md +58 -0
  152. package/docs/RELEASE-READINESS.md +95 -0
  153. package/docs/RELEASE-SECURITY.md +72 -0
  154. package/docs/RESILIENCE.md +160 -0
  155. package/docs/ROUTING.md +95 -0
  156. package/docs/SANDBOX-REVIEW.md +59 -0
  157. package/docs/SCAFFOLDING.md +72 -0
  158. package/docs/SECURITY-AUDIT.md +124 -0
  159. package/docs/SITE.md +150 -0
  160. package/docs/SPECIFICATION.md +232 -0
  161. package/docs/SPIKE-EXTENSIONS.md +475 -0
  162. package/docs/STANDARDS.md +303 -0
  163. package/docs/STARTERS.md +51 -0
  164. package/docs/TUNNELS.md +75 -0
  165. package/docs/TYPESCRIPT.md +102 -0
  166. package/docs/VERCEL.md +107 -0
  167. package/docs/YAML-GUIDE.md +521 -0
  168. package/docs/YAML-REFERENCE.md +328 -0
  169. package/docs/policies/agents.md +182 -0
  170. package/docs/policies/cache.md +139 -0
  171. package/docs/policies/compression.md +168 -0
  172. package/docs/policies/security.md +160 -0
  173. package/docs/policies/throttle.md +103 -0
  174. package/examples/assets/.env.example +1 -0
  175. package/examples/assets/.gitattributes +2 -0
  176. package/examples/assets/Makefile +30 -0
  177. package/examples/assets/README.md +18 -0
  178. package/examples/assets/functions/hello.mjs +3 -0
  179. package/examples/assets/gitignore.template +7 -0
  180. package/examples/assets/public/about.html +2 -0
  181. package/examples/assets/public/assets/example.txt +1 -0
  182. package/examples/assets/public/guide.txt +1 -0
  183. package/examples/assets/tests/requests.json +44 -0
  184. package/examples/assets/urlcode.yaml +27 -0
  185. package/examples/aws/README.md +13 -0
  186. package/examples/aws/handler.mjs +5 -0
  187. package/examples/aws/public/index.html +3 -0
  188. package/examples/aws/public/notes.txt +1 -0
  189. package/examples/aws/template.yaml +33 -0
  190. package/examples/aws/tests/requests.json +9 -0
  191. package/examples/aws/urlcode.yaml +19 -0
  192. package/examples/cloudflare/README.md +18 -0
  193. package/examples/cloudflare/package.json +14 -0
  194. package/examples/cloudflare/tests/requests.json +9 -0
  195. package/examples/cloudflare/urlcode.yaml +24 -0
  196. package/examples/cloudflare/wrangler.toml +5 -0
  197. package/examples/compliance/README.md +20 -0
  198. package/examples/compliance/rules.mjs +65 -0
  199. package/examples/cookbook/README.md +21 -0
  200. package/examples/cookbook/functions/choice.mjs +4 -0
  201. package/examples/cookbook/functions/echo.mjs +3 -0
  202. package/examples/cookbook/functions/hello.mjs +3 -0
  203. package/examples/cookbook/functions/text.mjs +3 -0
  204. package/examples/cookbook/middleware/headers.mjs +6 -0
  205. package/examples/cookbook/public/about.html +1 -0
  206. package/examples/cookbook/public/assets/index.html +1 -0
  207. package/examples/cookbook/public/assets/site.css +1 -0
  208. package/examples/cookbook/public/favicon.svg +1 -0
  209. package/examples/cookbook/public/guide.txt +1 -0
  210. package/examples/cookbook/public/llms.txt +6 -0
  211. package/examples/cookbook/routes/code.yaml +55 -0
  212. package/examples/cookbook/routes/files.yaml +17 -0
  213. package/examples/cookbook/routes/policies.yaml +28 -0
  214. package/examples/cookbook/routes/redirects.yaml +38 -0
  215. package/examples/cookbook/routes/responses.yaml +26 -0
  216. package/examples/cookbook/tests/requests.json +254 -0
  217. package/examples/cookbook/urlcode.yaml +22 -0
  218. package/examples/live-links/README.md +11 -0
  219. package/examples/live-links/tests/requests.json +6 -0
  220. package/examples/live-links/urlcode.yaml +16 -0
  221. package/examples/monitoring/blackbox-jobs.yaml +37 -0
  222. package/examples/monitoring/prometheus-rules.yaml +88 -0
  223. package/examples/monitoring/prometheus-scrape.yaml +20 -0
  224. package/examples/monitoring/vector.toml +65 -0
  225. package/examples/prerender/README.md +48 -0
  226. package/examples/prerender/functions/page.mjs +6 -0
  227. package/examples/prerender/middleware/template.mjs +29 -0
  228. package/examples/prerender/prerender.d.mts +4 -0
  229. package/examples/prerender/prerender.mjs +63 -0
  230. package/examples/prerender/tests/requests.json +39 -0
  231. package/examples/prerender/urlcode.yaml +50 -0
  232. package/examples/tunnel/dev-with-ngrok.sh +61 -0
  233. package/examples/vercel/README.md +13 -0
  234. package/examples/vercel/api/index.js +5 -0
  235. package/examples/vercel/package.json +8 -0
  236. package/examples/vercel/public/index.html +3 -0
  237. package/examples/vercel/public/notes.txt +1 -0
  238. package/examples/vercel/tests/requests.json +48 -0
  239. package/examples/vercel/urlcode.yaml +19 -0
  240. package/examples/vercel/vercel.json +10 -0
  241. package/llms.txt +46 -0
  242. package/package.json +123 -0
  243. package/schemas/urlcode.schema.json +1230 -0
  244. package/starters/default/.gitattributes +1 -0
  245. package/starters/default/Makefile +30 -0
  246. package/starters/default/README.md +34 -0
  247. package/starters/default/functions/hello.mjs +3 -0
  248. package/starters/default/gitignore.template +8 -0
  249. package/starters/default/middleware/headers.mjs +6 -0
  250. package/starters/default/routes/functions.yaml +20 -0
  251. package/starters/default/routes/marketing/links.yaml +7 -0
  252. package/starters/default/starter.json +5 -0
  253. package/starters/default/tests/requests.json +56 -0
  254. package/starters/default/urlcode.yaml +8 -0
@@ -0,0 +1,60 @@
1
+ {
2
+ "node": "v26.8.2",
3
+ "files": {
4
+ "dist/adapters.js": "40e5c2d0ff4be8b11752418f063a9e79e29e31b6a64c92a19a514f182268133c",
5
+ "dist/agent-lists.js": "fa1e87b928bf7178d7e910584018ef330a164be834156a8728f8d33ed7353211",
6
+ "dist/assets.js": "0833b093d2fe457ce68281110dc07d47914992753607741c13b339d92c76cf5c",
7
+ "dist/authoring.js": "99c3fe70df840ccc2a57b887b6b5886998a1155e848c3b6804a89bd1e257b05b",
8
+ "dist/aws.js": "1384e417de68404b5f1f35ee8a9148271b6efa0965a64e458d7bbda0234cc564",
9
+ "dist/build-cloudflare.js": "65af88384795bd7a4e2374c2cd9e5be174e3ffd4142f705723a3f5115a28eb27",
10
+ "dist/cli.js": "4cb9a0a6d85ac4b101b72cd71dbdb3ca17c8ab1d064986f9923e4a5254b8b7a9",
11
+ "dist/client-address.js": "292b86165def33247b05ef619e15245b70765522c8d9dc0ca4fac6e22e531de8",
12
+ "dist/cloudflare.js": "17b43b0a6b6a0ef8a893415c989e4ae3976d97cc966adbf897b785f9adc472e3",
13
+ "dist/compliance-rules/baseline.js": "6746ed9717fcbc414d4892ec54afca74496dcc725f2c80d417e382c190294811",
14
+ "dist/compliance-rules/privacy.js": "fe8330fd4c5068645e5c41db389fd190d7423e822397bf3938ca48d275bd449c",
15
+ "dist/compliance-rules/shared.js": "595a7ee3a5af8c52197c5c9cbf09f4289a234e52b7a198afdd498be9be8d28a1",
16
+ "dist/compliance-rules/strict.js": "937b55f4574eb5ca9f238b1a9658b089d006372328da35a329e4359719e6c3d0",
17
+ "dist/compliance.js": "e956dc89b0b729fed7031297b8d7554141110334e670edf5cedc3d052a8fb3e4",
18
+ "dist/config-worker.js": "bc2170d60c35f8d5de227cbfc4c5067dc61d7cbc5b98aed88e68a11797072da5",
19
+ "dist/config.js": "d0e0d100f3f97d3cefd17d898dddea463773092569a7cd7c3384bd547847659f",
20
+ "dist/errors.js": "a908e66496afebbdd632a8c159a6e14d23f8f38798a0b4342ca122f2511c6cde",
21
+ "dist/function-sources.js": "48b470fbdcef2ad2c83e61a6476d5499210d0937b72518ae01fb2b9225b0e16b",
22
+ "dist/function-worker.js": "3f5bad4f1b5cb812924c2dcc73ece6f816cf6aac26cd471fd1030dcfbf009c3b",
23
+ "dist/functions.js": "6b1e5d4a3f8db17d4e5df261b98ed15f5f0c5e5507a6fc0f472e9f94ed2b6090",
24
+ "dist/guest-api.js": "bc2501496a6ef9fd20dddb606fbc6fd557d534dd075e7ad38406248b230013ed",
25
+ "dist/header-validation.js": "465181dbb08ff05f52defdd29fda025c0c64589bf319fa87fa6d3ab4b68216d5",
26
+ "dist/http-policy.js": "4cb187642269078ddf3460a3f1813dc57878a616a6f8132ef8f8d3b4b91ebb56",
27
+ "dist/http-response.js": "5fc3bb9eb932a3fd2fbc72c361416a5f1439b8bf45b2be37baca1c6446abf40c",
28
+ "dist/index.js": "648878ef26cb3a8eb075a5ddd4d044022afe6cf5e5e38d1bd6ea418b82791371",
29
+ "dist/link-api.js": "0d1f2fb5ef8cd6fda7b071d3e08c53eb4b1c01d4003167af2f400949b70dea4f",
30
+ "dist/link-cli.js": "5f45e62a6b935112cabfcd12691a5ebc01d9c87f92e4befb655380d9e752323b",
31
+ "dist/link-events.js": "502a6038a538fa43094a9ba9c7546f504b4a1832a1377f26ca8cb69c2326670e",
32
+ "dist/link-records.js": "ef67a58cd107db0c3ac1c0ffd7b32817553606cf69bb3002da4ca0138c272f69",
33
+ "dist/link-store-worker.js": "0080452bbada2154b923449cd14596579bde94ad5f8dab94ecc84aa0d59c089b",
34
+ "dist/link-store.js": "526cdbe1b917cc7a74fb6edbbf13717d823ace5b5d6a5f89bc91438056a10b37",
35
+ "dist/logging.js": "8ec57fea26983aa071b05fe15d680d6550b03488101163ed9666da674118cf31",
36
+ "dist/management-policy.js": "ad573627de0e9f9b5368d701966468f391f53d64d2a82bfd8083bca412b01721",
37
+ "dist/match.js": "a0935ee5a3c68d85c268abbdcecd30a7ebd68e37bbecb68136479b1f94fc3705",
38
+ "dist/observability.js": "28a446ad867329e58cbccb9215a343a390e8599dad6a31c1db0e702d3df67e27",
39
+ "dist/plugins.js": "c228ec42cd727f259193fe905ae247f9fba34f695dd1364d8e3ee6683262da7c",
40
+ "dist/policies/agents.js": "761920214db2ada60733743541f134928ba1d2f0bc763660bd0d1112a725c887",
41
+ "dist/policies/cache.js": "ef16d7e592526c29f865b2693faa2c8afbea41c6526687563dffcb13a77fc1f7",
42
+ "dist/policies/compression.js": "d342c6db4a9bee63b62b35654dde28fa163bfd82510bb85331bb9b072f856781",
43
+ "dist/policies/security.js": "119fb37bd0e994ddabf3feacbc52d481c8e8c65b0ac6b71c8d3533b8c3e2cedb",
44
+ "dist/policies/throttle.js": "bbb0cb1c24cf70d0a1ac32fc0618efb6c44e5b5d42564842f9c585ff7e501603",
45
+ "dist/policies.js": "e3f48ad730d1d7b1435bff2f2bd7091e24a0c46dd0e1393d53337d64e3a7d83d",
46
+ "dist/policy.js": "280f94593c9e376726e2b3d34f4c1e9817802d67134bd6a2d1afe466244b3fdc",
47
+ "dist/prerender.js": "f568e2773be6fe67b547739f609c0287f429451ab31187f48ffe4560df3f877b",
48
+ "dist/project-tests.js": "91aa9c22d9f40efb0c5318b7a05b4aee753fc1ab591fe90e8d94f24fa60697f5",
49
+ "dist/readiness.js": "64eedecc9405db253e2bd39819c7f5640c6017f13cf23d2a193656dda095ca45",
50
+ "dist/router.js": "a102132362177bb8690bde11ce2b6ea9719a79601a25165d5d36dc736b1b888b",
51
+ "dist/runtime.js": "1d22cdb83703e0fc15da63d28b02fbd419051894063cb2fde5072b186c98ec62",
52
+ "dist/scaffold.js": "9dd30112e070ec554bd7cae1ca0a8b03642632be57411ba76a72972f4ba49b3e",
53
+ "dist/server.js": "d8c78d1695b3c11328f3f983281fba78c2aca6a66cf755b39ecf4ec2ec4b6f29",
54
+ "dist/site.js": "f34b023c9bcd403d38b8a2bf4d8ba184e0ff00cbd98405b49c68747466d995cd",
55
+ "dist/sqlite-version.js": "a3a08d99064bd0f35847e288027e00a516efdd61c675bfbbe7726e3aa900f0d1",
56
+ "dist/types.js": "96650efc60f2400b7b1a6622798f0f42d43f451fa5bf18a93d06482feb2e0283",
57
+ "dist/vercel.js": "76092b83e4d80e29541589682e00237701a3d3f096a8045fa2cf104b6d1b6725",
58
+ "dist/scripts/operational-drills.js": "b7e9448279c59ee9de62023055a9ff5c7a60bcceed4c33d023edc612f5575af9"
59
+ }
60
+ }
@@ -0,0 +1,59 @@
1
+ import { createRuntime } from './runtime.js';
2
+
3
+ import { validatePolicy } from './policy.js';
4
+ import { ConfigError } from './errors.js';
5
+
6
+
7
+ /** The subset of process.env a hosted adapter reads. */
8
+
9
+
10
+
11
+ // Handlers a stateless per-request invocation cannot honour. Functions and
12
+ // middleware need worker threads and the WASM engine on every cold start;
13
+ // stored links need a durable writable file that instances share. Adapters
14
+ // refuse them identically, so a project's supported surface does not depend on
15
+ // which provider is serving it.
16
+ const unsupported = { function:'isolated functions', link:'stored live links' };
17
+
18
+ export function readPolicyFromEnvironment(environment ) {
19
+ if (!environment.URLCODE_POLICY) return undefined;
20
+ let parsed ;
21
+ try { parsed = JSON.parse(environment.URLCODE_POLICY); }
22
+ catch { throw new ConfigError('URLCODE_POLICY is not valid JSON'); }
23
+ // The same grant document the self-hosted runtime reads from a file, carried
24
+ // through the only channel a managed platform has. Still revision-pinned.
25
+ return validatePolicy(parsed);
26
+ }
27
+
28
+ // Activates a project for a native-handler-only host, refusing the whole
29
+ // deployment rather than letting individual routes fail at request time.
30
+ export async function activateNativeOnly(project , environment , { target = 'node', plugins } = {}) {
31
+ const runtime = await createRuntime(project, { permissions: readPolicyFromEnvironment(environment), environment, target, plugins });
32
+ const refused = runtime.testPlan().inventory.flatMap(route => {
33
+ const reason = route.handler === undefined ? undefined : unsupported[route.handler];
34
+ return [
35
+ ...(reason ? [`${route.path} uses ${reason}`] : []),
36
+ ...(route.middleware ? [`${route.path} declares middleware`] : []),
37
+ ];
38
+ });
39
+ if (refused.length) {
40
+ await runtime.close();
41
+ throw new ConfigError(`This adapter serves native handlers only: ${refused.join('; ')}`);
42
+ }
43
+ return runtime;
44
+ }
45
+
46
+ // Caches a successful activation for the life of the instance. A failure is not
47
+ // cached, so a corrected deployment recovers without a code change.
48
+ export function lazyRuntime (activate ) {
49
+ let pending ;
50
+ return () => (pending ??= activate().catch((error ) => { pending = undefined; throw error; }));
51
+ }
52
+
53
+ export function resolveOrigin(origin , environment , platformVariables ) {
54
+ if (origin) return origin;
55
+ if (environment.URLCODE_ORIGIN) return environment.URLCODE_ORIGIN;
56
+ // Platform-set, not client-supplied: forwarded headers stay untrusted.
57
+ for (const name of platformVariables) if (environment[name]) return `https://${environment[name]}`;
58
+ return undefined;
59
+ }
@@ -0,0 +1,59 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve, relative, isAbsolute, sep } from 'node:path';
3
+ import { assert, ConfigError } from './errors.js';
4
+ import { isListFile, isBundled, validatePattern } from './policies/agents.js';
5
+
6
+
7
+ // Node-side loader for project-relative agent lists. The policy module itself
8
+ // has no filesystem access (it also runs in the Worker), so it delegates here
9
+ // on Node and expects the Worker build to attach the same data up front.
10
+ //
11
+ // A list file is the schema data/agents/*.json uses: either an array of
12
+ // entries or `{ entries: [...] }`, each entry `{ name?, pattern, ... }`.
13
+ export const MAX_LIST_ENTRIES = 4096;
14
+
15
+ function listPath(root , reference , routePattern ) {
16
+ assert(isListFile(reference) && !isAbsolute(reference), `${routePattern}: policies.agents list ${JSON.stringify(reference)} must be a project-relative path ending in .json`);
17
+ const path = resolve(root, reference);
18
+ const inside = relative(root, path);
19
+ assert(inside && !inside.startsWith('..') && !inside.split(sep).includes('..'), `${routePattern}: policies.agents list ${JSON.stringify(reference)} must stay inside the project`);
20
+ return path;
21
+ }
22
+
23
+ export async function loadListFile(root , reference , routePattern = 'project') {
24
+ const path = listPath(root, reference, routePattern);
25
+ let text ;
26
+ try { text = await readFile(path, 'utf8'); } catch (error) { const failure = error ; throw new ConfigError(`${routePattern}: policies.agents list ${JSON.stringify(reference)} cannot be read (${failure.code ?? failure.message})`); }
27
+ let parsed ;
28
+ try { parsed = JSON.parse(text); } catch (error) { throw new ConfigError(`${routePattern}: policies.agents list ${JSON.stringify(reference)} is not valid JSON (${(error ).message})`); }
29
+ const entries = Array.isArray(parsed) ? parsed : (parsed )?.entries;
30
+ assert(Array.isArray(entries), `${routePattern}: policies.agents list ${JSON.stringify(reference)} must be an array of entries or {"entries": [...]}`);
31
+ assert(entries.length <= MAX_LIST_ENTRIES, `${routePattern}: policies.agents list ${JSON.stringify(reference)} exceeds ${MAX_LIST_ENTRIES} entries`);
32
+ return entries.map((entry , index) => {
33
+ const record = entry ;
34
+ assert(record && typeof record === 'object' && typeof record.pattern === 'string', `${routePattern}: policies.agents list ${JSON.stringify(reference)} entry ${index} needs a string "pattern"`);
35
+ const problem = validatePattern(record.pattern);
36
+ assert(!problem, `${routePattern}: policies.agents list ${JSON.stringify(reference)} entry ${index} pattern ${JSON.stringify(record.pattern)} rejected (${problem})`);
37
+ return { name: typeof record.name === 'string' ? record.name : record.pattern, pattern: record.pattern };
38
+ });
39
+ }
40
+
41
+ // Loads every `.json` reference once; bundled names need no loading.
42
+ export async function loadListFiles(references , root , routePattern ) {
43
+ assert(root, `${routePattern}: policies.agents list files need a project root`);
44
+ const loaded = {};
45
+ for (const reference of new Set(references)) {
46
+ if (isBundled(reference) || !isListFile(reference)) continue;
47
+ loaded[reference] = await loadListFile(root, reference, routePattern);
48
+ }
49
+ return loaded;
50
+ }
51
+
52
+ // For the Cloudflare build: returns the configuration with its project list
53
+ // files attached as `resolved`, so the Worker's synchronous compile finds them
54
+ // in the artifact. Bundled names are left alone; the Worker carries those.
55
+ export async function resolveLists(config , root , routePattern = 'project') {
56
+ const references = [...(config?.deny ?? []), ...(config?.allow ?? [])].filter(isListFile);
57
+ if (!references.length) return config;
58
+ return { ...config, resolved: await loadListFiles(references, root, routePattern) };
59
+ }
package/dist/assets.js ADDED
@@ -0,0 +1,129 @@
1
+ import { lstat, readdir, open } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import { join, basename } from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import mime from 'mime-types';
6
+ import { create as disposition } from 'content-disposition';
7
+ import { assert, HttpError } from './errors.js';
8
+
9
+
10
+
11
+ /** The header surface assetResponse reads for conditional and range requests. */
12
+
13
+
14
+
15
+ // Asset bytes are immutable between successful reloads. No request opens a file.
16
+ const denied = /^(?:node_modules|urlcode\.ya?ml|package(?:-lock)?\.json|.*\.(?:pem|key|p12|pfx|env))$/i;
17
+ // The same rule the static walker applies: hidden and sensitive names are
18
+ // never published, so a sitemap built from a directory must skip them too.
19
+ export const publishableAssetName = (name ) => !name.startsWith('.') && !denied.test(name);
20
+ function partsFor(value ) {
21
+ const parts = value.split('/');
22
+ assert(parts.every(p => p && !p.startsWith('.') && !denied.test(p) && !/[\\:\u0000-\u001f\u007f]/u.test(p)), 'Unsafe asset path; use a dedicated public asset directory');
23
+ return parts;
24
+ }
25
+ export async function compileAssets(root , routes ) {
26
+ const cache = new Map (), watch = [], digest = createHash('sha256'); let bytes = 0, entries = 0;
27
+ async function checked(relative ) {
28
+ let file = root;
29
+ for (const part of partsFor(relative)) {
30
+ file = join(file,part);
31
+ const stat = await lstat(file);
32
+ assert(!stat.isSymbolicLink(), 'Asset symlinks are forbidden');
33
+ }
34
+ return file;
35
+ }
36
+ async function read(relative , config , download ) {
37
+ const file = await checked(relative);
38
+ let saved = cache.get(file);
39
+ if (!saved) {
40
+ assert((await lstat(file)).isFile(), 'Assets must be regular files');
41
+ const handle = await open(file, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
42
+ try {
43
+ const stat = await handle.stat();
44
+ assert(stat.isFile() && stat.nlink === 1, 'Assets must be regular files without hard links');
45
+ assert(stat.size <= 16 * 1024 * 1024 && bytes + stat.size <= 64 * 1024 * 1024, 'Asset snapshot exceeds 16 MiB per file or 64 MiB total');
46
+ // Read at most the inspected size; reject concurrent growth or replacement.
47
+ const body = Buffer.alloc(stat.size + 1);
48
+ let length = 0;
49
+ while (length < body.length) { const result = await handle.read(body,length,body.length-length,length); if (!result.bytesRead) break; length += result.bytesRead; }
50
+ const after = await handle.stat();
51
+ assert(length === stat.size && after.mtimeMs === stat.mtimeMs && after.ctimeMs === stat.ctimeMs, 'Asset changed during snapshot');
52
+ saved = { body:body.subarray(0,length), modified:new Date(Math.floor(stat.mtimeMs/1000)*1000).toUTCString() };
53
+ bytes += length; cache.set(file,saved);
54
+ } finally { await handle.close(); }
55
+ }
56
+ const type = mime.contentType(config.contentType || mime.lookup(relative) || 'application/octet-stream');
57
+ assert(type, 'Asset content type is not a media type'); // unreachable: the schema requires a type/subtype form
58
+ const filename = ('filename' in config && config.filename) || basename(relative);
59
+ assert(!/[/\\\u0000-\u001f\u007f]/u.test(filename), 'Download filename must be a safe basename');
60
+ const attachment = download ? disposition(filename) : undefined;
61
+ const etag = '"' + createHash('sha256').update(type + (attachment || '')).update(saved.body).digest('hex') + '"';
62
+ digest.update(relative + etag);
63
+ return { ...saved, type, attachment, etag, cache:config.cacheControl || 'no-cache' };
64
+ }
65
+ for (const route of routes) {
66
+ const config = route.static;
67
+ if (!config) {
68
+ const file = route.page || route.download;
69
+ if (!file) continue;
70
+ watch.push(await checked(file.file)); route.asset = await read(file.file,file,Boolean(route.download)); continue;
71
+ }
72
+ const mount = config; // a const the hoisted walk() below sees as narrowed
73
+ const dir = await checked(mount.directory); watch.push(dir);
74
+ assert((await lstat(dir)).isDirectory(), 'Static directory must exist');
75
+ const files = new Map ();
76
+ async function walk(relative , key = '', depth = 0) {
77
+ assert(depth <= 20, 'Asset directory depth exceeded');
78
+ for (const entry of (await readdir(await checked(relative),{withFileTypes:true})).sort((a,b) => a.name.localeCompare(b.name))) {
79
+ assert(++entries <= 10000, 'Maximum 10000 asset entries');
80
+ // Hidden/sensitive files are never published, even inside a public directory.
81
+ if (entry.name.startsWith('.') || denied.test(entry.name)) continue;
82
+ const next = relative + '/' + entry.name, name = key + entry.name;
83
+ if (entry.isDirectory()) await walk(next,name+'/',depth+1);
84
+ else files.set(name,await read(next,mount,false));
85
+ }
86
+ }
87
+ await walk(mount.directory);
88
+ route.asset = files;
89
+ }
90
+ return { watch, digest:digest.digest('hex').slice(0,16) };
91
+ }
92
+ export function assetResponse(route , path , method , request ) {
93
+ let asset = route.asset;
94
+ if (route.static) {
95
+ let key = path.slice(route.prefix .length);
96
+ if (key.endsWith('/') || !key) key += route.static.index || '';
97
+ asset = asset instanceof Map ? asset.get(key) : undefined;
98
+ if (!asset) throw new HttpError(404, 'Not found');
99
+ }
100
+ assert(asset && !(asset instanceof Map), 'Route has no asset snapshot');
101
+ const { body, etag, modified } = asset;
102
+ const headers = [['content-type',asset.type],['etag',etag],['last-modified',modified],['cache-control',asset.cache],['accept-ranges','bytes']];
103
+ if (asset.attachment) headers.push(['content-disposition',asset.attachment]);
104
+ const empty = (status ) => ({ status, headers, body:Buffer.alloc(0), ...(status === 304 ? {} : {contentLength:0}) });
105
+ const matches = (value , weak ) => value.trim() === '*' || value.split(',').some(tag => (weak ? tag.trim().replace(/^W\//,'') : tag.trim()) === etag);
106
+ const match = request.get('if-match'), none = request.get('if-none-match');
107
+ if (match && !matches(match,false)) return empty(412);
108
+ if (!match && request.has('if-unmodified-since') && Date.parse(modified) > Date.parse(request.get('if-unmodified-since') )) return empty(412);
109
+ if (none ? matches(none,true) : request.has('if-modified-since') && Date.parse(modified) <= Date.parse(request.get('if-modified-since') )) return empty(304);
110
+ const range = request.get('range');
111
+ // Multiple/malformed ranges and date If-Range fall back to the full representation.
112
+ if (method === 'GET' && range && (!request.has('if-range') || request.get('if-range') === etag)) {
113
+ const m = /^bytes=(\d*)-(\d*)$/.exec(range);
114
+ if (m && (m[1] || m[2])) {
115
+ const size = BigInt(body.length);
116
+ let start = m[1] ? BigInt(m[1]) : size - BigInt(m[2] );
117
+ let end = m[1] && m[2] ? BigInt(m[2]) : size - 1n;
118
+ if (start < 0n) start = 0n;
119
+ if (end >= size) end = size - 1n;
120
+ if (start >= size || end < start) { headers.push(['content-range',`bytes */${size}`]); return empty(416); }
121
+ headers.push(['content-range',`bytes ${start}-${end}/${size}`]);
122
+ const part = body.subarray(Number(start),Number(end)+1);
123
+ return {status:206,headers,body:part,contentLength:part.length};
124
+ }
125
+ }
126
+ // The snapshot reference lets the compression policy serve a precomputed
127
+ // variant by identity check instead of recompressing immutable bytes.
128
+ return {status:200,headers,body,contentLength:body.length,asset};
129
+ }
@@ -0,0 +1,68 @@
1
+ import { cp, readdir, mkdir, mkdtemp, rename, rm, readFile, open } from 'node:fs/promises';
2
+ import { resolve, dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { randomBytes } from 'node:crypto';
5
+ import { parseDocument } from 'yaml';
6
+ import { loadDocument, validateDocument } from './config.js';
7
+ import { compileRoutes } from './router.js';
8
+ import { prepareFunctionSnapshot, requestedPermissions } from './policy.js';
9
+ import { assert } from './errors.js';
10
+
11
+
12
+ export async function initProject(destination ) {
13
+ const target = resolve(destination);
14
+ await mkdir(dirname(target), { recursive: true });
15
+ // Reserve destination before copying; never merge into existing user files.
16
+ await mkdir(target);
17
+ try {
18
+ const source = fileURLToPath(new URL('../starters/default/', import.meta.url));
19
+ for (const file of await readdir(source)) {
20
+ if (file === '.gitignore') continue;
21
+ await cp(join(source,file), join(target,file === 'gitignore.template' ? '.gitignore' : file), { recursive: true, force: false, errorOnExist: true });
22
+ }
23
+ } catch (error) { await rm(target, { recursive: true, force: true }); throw error; }
24
+ return target;
25
+ }
26
+ export async function addRedirect(project , destination , alias ) {
27
+ const loaded = await loadDocument(project);
28
+ const slug = alias || randomBytes(6).toString('base64url');
29
+ assert(/^[A-Za-z0-9_-]{1,128}$/.test(slug), 'Alias must contain 1–128 letters, digits, underscores or hyphens');
30
+ const pattern = '/' + slug;
31
+ assert(!Object.hasOwn(loaded.routes, pattern), 'Alias already exists');
32
+ const lockPath = join(loaded.root, 'urlcode.yaml.lock');
33
+ const lock = await open(lockPath, 'wx', 0o600);
34
+ let temp ;
35
+ try {
36
+ const file = join(loaded.root,'urlcode.yaml');
37
+ const original = await readFile(file, 'utf8');
38
+ // Reload under the lock to prevent competing authoring commands losing changes.
39
+ const latest = await loadDocument(loaded.root);
40
+ assert(!Object.hasOwn(latest.routes, pattern), 'Alias already exists');
41
+ const doc = parseDocument(original, { uniqueKeys:false });
42
+ doc.setIn(['routes', pattern], { redirect: { url: destination, status: 302 } });
43
+ const data = validateDocument(doc.toJS());
44
+ const added = data.routes[pattern];
45
+ assert(added, 'Alias was not written');
46
+ const routes = { ...latest.routes, [pattern]: added };
47
+ const candidate = { ...latest, routes };
48
+ const snapshot = await prepareFunctionSnapshot(candidate);
49
+ // Authoring checks shape/references with dummy values; it must neither read
50
+ // credentials nor execute code. This does not create an operator grant.
51
+ const bindings = Object.create(null) ;
52
+ for (const route of Object.values(routes)) {
53
+ for (const ref of Object.values(route.env || {})) if (ref.env) bindings[ref.env] = 'validation-only';
54
+ for (const ref of Object.values(route.secrets || {})) bindings[ref.secret] = 'validation-only';
55
+ }
56
+ await compileRoutes(candidate, bindings, requestedPermissions(candidate,snapshot), snapshot.projectSha256);
57
+ temp = await mkdtemp(join(loaded.root, '.urlcode-edit-'));
58
+ const temporary = join(temp, 'urlcode.yaml');
59
+ const out = await open(temporary, 'wx', 0o600);
60
+ try { await out.writeFile(String(doc)); await out.sync(); } finally { await out.close(); }
61
+ assert(await readFile(file,'utf8') === original, 'Configuration changed during edit; retry');
62
+ await rename(temporary, file);
63
+ return pattern;
64
+ } finally {
65
+ if (temp) await rm(temp, { recursive: true, force: true });
66
+ await lock.close(); await rm(lockPath, { force: true });
67
+ }
68
+ }
package/dist/aws.js ADDED
@@ -0,0 +1,110 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { activateNativeOnly, lazyRuntime, resolveOrigin } from './adapters.js';
3
+
4
+
5
+ import { prepareResponse, errorResponse } from './http-response.js';
6
+
7
+ import { assert, ConfigError, HttpError } from './errors.js';
8
+
9
+
10
+ /** A Lambda payload format 2.0 event, as far as this adapter reads it. */
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+ const platformOrigins = ['URLCODE_PUBLIC_HOST'];
20
+ const isRecord = (value ) => value !== null && typeof value === 'object';
21
+
22
+ // Payload format 2.0 only, as used by Lambda Function URLs and API Gateway
23
+ // HTTP APIs. Format 1.0 supplies the path and query already decoded, so the
24
+ // original bytes cannot be recovered; this runtime rejects ambiguous encoding
25
+ // deliberately, and rebuilding a target from decoded parts would either
26
+ // re-encode differently or quietly accept what the runtime refuses. Front a
27
+ // REST API with an HTTP API, or run the container image with `urlcode serve`.
28
+ function target(event ) {
29
+ if (event.version !== '2.0') {
30
+ throw new ConfigError(event.httpMethod
31
+ ? 'Payload format 1.0 is unsupported because it cannot preserve the original request encoding; use a Function URL or HTTP API'
32
+ : 'Unsupported Lambda event: this adapter expects payload format 2.0');
33
+ }
34
+ const method = event.requestContext?.http?.method;
35
+ assert(typeof method === 'string' && method.length, 'Lambda event has no request method');
36
+ const path = typeof event.rawPath === 'string' && event.rawPath.length ? event.rawPath : '/';
37
+ const query = typeof event.rawQueryString === 'string' ? event.rawQueryString : '';
38
+ return { method, target: query ? `${path}?${query}` : path };
39
+ }
40
+
41
+ function requestHeaders(event ) {
42
+ const headers = new Headers(), counts = Object.create(null) ;
43
+ for (const [name,value] of Object.entries(event.headers || {})) {
44
+ if (value === undefined) continue;
45
+ const key = name.toLowerCase();
46
+ // API Gateway joins repeats with a comma; the count reflects that a client
47
+ // sent more than one, which route policy uses to reject ambiguous inputs.
48
+ const parts = key === 'cookie' ? [value] : String(value).split(',');
49
+ for (const part of parts) headers.append(key, part.trim());
50
+ counts[key] = parts.length;
51
+ }
52
+ for (const cookie of event.cookies || []) headers.append('cookie', cookie);
53
+ if (event.cookies?.length) counts.cookie = (counts.cookie || 0) + event.cookies.length;
54
+ return { headers, counts };
55
+ }
56
+
57
+ function requestBody(event , limit ) {
58
+ if (event.body === undefined || event.body === null) return Buffer.alloc(0);
59
+ const body = event.isBase64Encoded ? Buffer.from(event.body,'base64') : Buffer.from(event.body,'utf8');
60
+ if (body.length > limit) throw new HttpError(413,'Request body too large');
61
+ return body;
62
+ }
63
+
64
+ // Builds a Lambda handler for payload format 2.0. The runtime is created once
65
+ // per execution environment and reused across warm invocations.
66
+ export function createLambdaHandler({ project = process.cwd(), origin, environment = process.env,
67
+ maxBodyBytes = 1048576, plugins } = {}) {
68
+ assert(Number.isInteger(maxBodyBytes) && maxBodyBytes >= 1 && maxBodyBytes <= 16777216, 'Request limit must be 1–16777216 bytes');
69
+ const ready = lazyRuntime(() => activateNativeOnly(project, environment, { target: 'aws', plugins }));
70
+
71
+ return async function handler(raw) {
72
+ const requestId = randomUUID();
73
+ let method = 'GET', runtime ;
74
+ try {
75
+ runtime = await ready();
76
+ assert(isRecord(raw), 'Lambda event must be an object');
77
+ const event = raw ; // trust boundary: the platform's event, checked field by field below
78
+ const request = target(event);
79
+ method = request.method;
80
+ const { headers, counts } = requestHeaders(event);
81
+ const limit = Math.min(maxBodyBytes, runtime.requestLimit(request.target) ?? maxBodyBytes);
82
+ const result = await runtime.handle({ target:request.target, method, headers, headerCounts:counts,
83
+ body: requestBody(event,limit),
84
+ origin: resolveOrigin(origin,environment,platformOrigins) ?? 'http://localhost',
85
+ // Set by the platform from the connection, not by the client.
86
+ client: event.requestContext?.http?.sourceIp });
87
+ return respond(prepareResponse(result,{ requestId, method }));
88
+ } catch (error) {
89
+ // An activation or configuration failure is the operator's to read in the
90
+ // function log; a request only ever learns the status.
91
+ if (!(error instanceof HttpError)) console.error(error);
92
+ const prepared = errorResponse(error, { requestId, method,
93
+ headers: runtime?.errorHeaders(error, resolveOrigin(origin,environment,platformOrigins) ?? 'http://localhost') ?? [] });
94
+ return respond({ ...prepared, cookies: [], body: prepared.body === undefined ? undefined : Buffer.from(prepared.body) });
95
+ }
96
+ };
97
+ }
98
+
99
+ function respond({ status, headers, cookies, body } ) {
100
+ const merged = {};
101
+ for (const [key,value] of headers) {
102
+ const name = key.toLowerCase();
103
+ const previous = merged[name];
104
+ merged[name] = previous === undefined ? value : `${previous}, ${value}`;
105
+ }
106
+ // Always base64: a response may carry image or archive bytes, and guessing
107
+ // whether a body is text is how binary assets get corrupted in transit.
108
+ return { statusCode: status, headers: merged, cookies,
109
+ body: Buffer.from(body ?? Buffer.alloc(0)).toString('base64'), isBase64Encoded: true };
110
+ }