@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,254 @@
1
+ [
2
+ {
3
+ "path": "/hello/Ada",
4
+ "status": 200,
5
+ "expectBody": "{\"message\":\"Hello, Ada.\"}",
6
+ "expectHeaders": {
7
+ "x-middleware": "cookbook",
8
+ "x-app": "cookbook"
9
+ }
10
+ },
11
+ {
12
+ "path": "/hello/Ada?excited=true",
13
+ "status": 200,
14
+ "expectBody": "{\"message\":\"Hello, Ada!\"}"
15
+ },
16
+ {
17
+ "path": "/hello/Ada",
18
+ "status": 200,
19
+ "method": "HEAD",
20
+ "expectBody": "",
21
+ "expectHeaders": {
22
+ "x-middleware": "cookbook"
23
+ }
24
+ },
25
+ {
26
+ "path": "/hello/Ada?excited=maybe",
27
+ "status": 400
28
+ },
29
+ {
30
+ "path": "/echo",
31
+ "method": "POST",
32
+ "headers": {
33
+ "content-type": "application/json"
34
+ },
35
+ "body": "{\"ok\":true}",
36
+ "status": 200,
37
+ "expectBody": "{\"ok\":true}"
38
+ },
39
+ {
40
+ "path": "/echo",
41
+ "method": "POST",
42
+ "headers": {
43
+ "content-type": "application/json"
44
+ },
45
+ "body": "{",
46
+ "status": 400
47
+ },
48
+ {
49
+ "path": "/echo",
50
+ "method": "POST",
51
+ "headers": {
52
+ "content-type": "text/plain"
53
+ },
54
+ "body": "hello",
55
+ "status": 415
56
+ },
57
+ {
58
+ "path": "/text",
59
+ "method": "POST",
60
+ "headers": {
61
+ "content-type": "text/plain"
62
+ },
63
+ "body": "hello",
64
+ "status": 200,
65
+ "expectBody": "hello"
66
+ },
67
+ {
68
+ "path": "/choice",
69
+ "status": 302,
70
+ "method": "GET",
71
+ "expectHeaders": {
72
+ "location": "https://example.com/docs"
73
+ }
74
+ },
75
+ {
76
+ "path": "/product/abc?page=2&tag=red&tag=blue&utm_source=news&ignored=no",
77
+ "status": 302,
78
+ "method": "GET",
79
+ "expectHeaders": {
80
+ "location": "https://example.com/products/abc?page=2&label=red&label=blue&channel=web&utm_source=news"
81
+ }
82
+ },
83
+ {
84
+ "path": "/choice",
85
+ "status": 302,
86
+ "method": "HEAD",
87
+ "expectHeaders": {
88
+ "location": "https://example.com/docs"
89
+ }
90
+ },
91
+ {
92
+ "path": "/product/abc?page=2&tag=red&tag=blue&utm_source=news&ignored=no",
93
+ "status": 302,
94
+ "method": "HEAD",
95
+ "expectHeaders": {
96
+ "location": "https://example.com/products/abc?page=2&label=red&label=blue&channel=web&utm_source=news"
97
+ }
98
+ },
99
+ {
100
+ "path": "/product/abc?page=0",
101
+ "status": 400
102
+ },
103
+ {
104
+ "path": "/preflight",
105
+ "status": 204,
106
+ "method": "OPTIONS",
107
+ "expectBody": "",
108
+ "expectHeaders": {
109
+ "access-control-allow-origin": "https://app.example.com"
110
+ }
111
+ },
112
+ {
113
+ "path": "/download",
114
+ "status": 200,
115
+ "expectBody": "URLCode guide\n",
116
+ "expectHeaders": {
117
+ "content-disposition": "attachment; filename=urlcode-guide.txt"
118
+ }
119
+ },
120
+ {
121
+ "path": "/assets/site.css",
122
+ "status": 206,
123
+ "expectBody": "body",
124
+ "expectHeaders": {
125
+ "content-range": "bytes 0-3/22"
126
+ },
127
+ "headers": {
128
+ "range": "bytes=0-3"
129
+ }
130
+ },
131
+ {
132
+ "path": "/paused",
133
+ "status": 404
134
+ },
135
+ {
136
+ "path": "/expired",
137
+ "status": 410
138
+ },
139
+ {
140
+ "path": "/missing",
141
+ "status": 404
142
+ },
143
+ {
144
+ "path": "/secure",
145
+ "status": 200,
146
+ "expectBody": "Hardened response",
147
+ "expectHeaders": {
148
+ "referrer-policy": "no-referrer",
149
+ "x-frame-options": "deny",
150
+ "cross-origin-opener-policy": "same-origin"
151
+ }
152
+ },
153
+ {
154
+ "path": "/secure",
155
+ "method": "HEAD",
156
+ "status": 200,
157
+ "expectBody": "",
158
+ "expectHeaders": {
159
+ "x-frame-options": "deny"
160
+ }
161
+ },
162
+ {
163
+ "path": "/no-bots",
164
+ "status": 302,
165
+ "expectHeaders": {
166
+ "location": "https://example.com/article"
167
+ }
168
+ },
169
+ {
170
+ "path": "/no-bots",
171
+ "method": "HEAD",
172
+ "status": 302
173
+ },
174
+ {
175
+ "path": "/no-bots",
176
+ "headers": {
177
+ "user-agent": "Mozilla/5.0 (compatible; GPTBot/1.0; +https://openai.com/gptbot)"
178
+ },
179
+ "status": 403
180
+ },
181
+ {
182
+ "path": "/no-bots",
183
+ "headers": {
184
+ "user-agent": "curl/8.0"
185
+ },
186
+ "status": 403
187
+ },
188
+ {
189
+ "path": "/no-bots",
190
+ "headers": {
191
+ "user-agent": "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
192
+ },
193
+ "status": 302
194
+ },
195
+ {
196
+ "path": "/cached",
197
+ "status": 200,
198
+ "expectBody": "{\"cached\":true}",
199
+ "expectHeaders": {
200
+ "cache-control": "public, max-age=60"
201
+ }
202
+ },
203
+ {
204
+ "path": "/cached",
205
+ "method": "HEAD",
206
+ "status": 200,
207
+ "expectBody": ""
208
+ },
209
+ {
210
+ "path": "/budget",
211
+ "status": 200,
212
+ "expectBody": "Budgeted",
213
+ "expectHeaders": {
214
+ "ratelimit-policy": "\"default\";q=100;w=60"
215
+ }
216
+ },
217
+ {
218
+ "path": "/budget",
219
+ "method": "HEAD",
220
+ "status": 200,
221
+ "expectBody": ""
222
+ },
223
+ {
224
+ "path": "/robots.txt",
225
+ "status": 200,
226
+ "expectHeaders": {
227
+ "content-type": "text/plain; charset=utf-8"
228
+ }
229
+ },
230
+ {
231
+ "path": "/.well-known/security.txt",
232
+ "status": 200,
233
+ "expectBody": "Contact: mailto:security@example.com\nExpires: 2099-01-01T00:00:00Z\nPolicy: https://example.com/security-policy\nPreferred-Languages: en\n"
234
+ },
235
+ {
236
+ "path": "/favicon.ico",
237
+ "status": 200,
238
+ "expectHeaders": {
239
+ "content-type": "image/svg+xml",
240
+ "cache-control": "public, max-age=3600"
241
+ }
242
+ },
243
+ {
244
+ "path": "/llms.txt",
245
+ "status": 200,
246
+ "expectHeaders": {
247
+ "content-type": "text/plain; charset=utf-8"
248
+ }
249
+ },
250
+ {
251
+ "path": "/sitemap.xml",
252
+ "status": 404
253
+ }
254
+ ]
@@ -0,0 +1,22 @@
1
+ # yaml-language-server: $schema=https://raw.githubusercontent.com/jimhoyd-com/urlcode/669632fb4f188f293a0f622847f87ee019c0627f/schemas/urlcode.schema.json
2
+ version: "1"
3
+ includes:
4
+ - routes/code.yaml
5
+ - routes/redirects.yaml
6
+ - routes/responses.yaml
7
+ - routes/files.yaml
8
+ - routes/policies.yaml
9
+ routes: {}
10
+ # Site conventions: each key generates one native route (see docs/SITE.md).
11
+ # sitemap is left out here because the cookbook audit runs without --origin.
12
+ site:
13
+ robots:
14
+ disallow: [ai-crawlers, /admin]
15
+ sitemap: true
16
+ favicon: public/favicon.svg
17
+ securityTxt:
18
+ contact: [mailto:security@example.com]
19
+ expires: "2099-01-01T00:00:00Z"
20
+ policy: [https://example.com/security-policy]
21
+ preferredLanguages: [en]
22
+ llms: public/llms.txt
@@ -0,0 +1,11 @@
1
+ # Live links without route reloads
2
+
3
+ Follow [the dynamic-link guide](../../docs/DYNAMIC-LINKS.md) to create an external
4
+ SQLite store and seed `demo -> https://example.com/demo`. Then start this project
5
+ with `--link-store links=/absolute/path/links.sqlite`. Its fixtures expect that
6
+ seed and an unused `not-created` code; use a disposable test store, not production.
7
+
8
+ Create/update/delete records through the CLI or separate authenticated API while
9
+ the public server keeps serving. YAML, code, route counts and health version do
10
+ not change per link. This example deliberately requires explicit operator storage
11
+ binding; the default starter still runs without a database.
@@ -0,0 +1,6 @@
1
+ [
2
+ {"path":"/r/demo","status":302,"expectHeaders":{"location":"https://example.com/demo","cache-control":"no-store"}},
3
+ {"path":"/r/demo","method":"HEAD","status":302,"expectBody":"","expectHeaders":{"location":"https://example.com/demo"}},
4
+ {"path":"/r/not-created","status":404},
5
+ {"path":"/r/demo","method":"POST","status":405,"expectHeaders":{"allow":"GET, HEAD"}}
6
+ ]
@@ -0,0 +1,16 @@
1
+ # yaml-language-server: $schema=https://raw.githubusercontent.com/jimhoyd-com/urlcode/669632fb4f188f293a0f622847f87ee019c0627f/schemas/urlcode.schema.json
2
+ version: "1"
3
+ dynamicLinks: true
4
+ routes:
5
+ /r/{code}:
6
+ parameters:
7
+ - name: code
8
+ in: path
9
+ required: true
10
+ schema: {type: string, minLength: 1, maxLength: 128}
11
+ link:
12
+ collection: links
13
+ code: {from: path, name: code}
14
+ /about:
15
+ respond:
16
+ text: Links are stored separately from this YAML.
@@ -0,0 +1,37 @@
1
+ # Prometheus scrape jobs for URLCode's two probes, via blackbox_exporter.
2
+ #
3
+ # Keep both: /_urlcode/health answers whenever the process is alive, while
4
+ # /_urlcode/ready reports 503 whenever a function worker or link-store reader is
5
+ # unavailable. Alerting only on health hides a process that is up and serving
6
+ # nothing; alerting only on ready pages for a brief, self-healing replacement.
7
+ #
8
+ # Both probes are unauthenticated and report the configuration digest and route
9
+ # count. Keep them on an internal interface or restrict them at the ingress.
10
+ scrape_configs:
11
+ - job_name: urlcode-ready
12
+ metrics_path: /probe
13
+ params:
14
+ module: [http_2xx]
15
+ static_configs:
16
+ - targets: ['http://urlcode.internal:3000/_urlcode/ready']
17
+ relabel_configs:
18
+ - source_labels: [__address__]
19
+ target_label: __param_target
20
+ - source_labels: [__param_target]
21
+ target_label: instance
22
+ - target_label: __address__
23
+ replacement: blackbox:9115
24
+
25
+ - job_name: urlcode-health
26
+ metrics_path: /probe
27
+ params:
28
+ module: [http_2xx]
29
+ static_configs:
30
+ - targets: ['http://urlcode.internal:3000/_urlcode/health']
31
+ relabel_configs:
32
+ - source_labels: [__address__]
33
+ target_label: __param_target
34
+ - source_labels: [__param_target]
35
+ target_label: instance
36
+ - target_label: __address__
37
+ replacement: blackbox:9115
@@ -0,0 +1,88 @@
1
+ # Alert rules for a URLCode deployment.
2
+ #
3
+ # These rules read blackbox probes of the two health endpoints and counters
4
+ # derived from the JSON request log by a log pipeline (see vector.toml in this
5
+ # directory). Where the built-in endpoint (prometheus-scrape.yaml, enabled with
6
+ # startServer({ metrics: true })) offers the same signal, a comment gives the
7
+ # equivalent expression; it labels by status_class rather than exact status.
8
+ # Rule names are explained in docs/MONITORING.md.
9
+ groups:
10
+ - name: urlcode-availability
11
+ interval: 30s
12
+ rules:
13
+ - alert: UrlcodeNotReady
14
+ # /_urlcode/ready is 503 while a function worker or link-store reader is
15
+ # being replaced. Brief flaps are normal during replacement; sustained
16
+ # means replacement is not converging.
17
+ expr: probe_success{job="urlcode-ready"} == 0
18
+ for: 5m
19
+ labels:
20
+ severity: page
21
+ annotations:
22
+ summary: "URLCode has not been ready for 5 minutes"
23
+ runbook: "Check function_worker and link_store_worker records for repeated restarts."
24
+
25
+ - alert: UrlcodeDown
26
+ # /_urlcode/health answers whenever the process is alive and not shutting
27
+ # down. Failing it is a process-level problem, not a capacity one.
28
+ expr: probe_success{job="urlcode-health"} == 0
29
+ for: 2m
30
+ labels:
31
+ severity: page
32
+ annotations:
33
+ summary: "URLCode is not answering liveness probes"
34
+ runbook: "The process is gone, wedged or out of file descriptors. Check the supervisor."
35
+
36
+ - name: urlcode-serving
37
+ interval: 30s
38
+ rules:
39
+ - alert: UrlcodeServerErrors
40
+ # 5xx as a share of served requests, by matched route pattern.
41
+ # Built-in endpoint (no per-route status split):
42
+ # sum(rate(urlcode_requests_total{status_class="5xx"}[5m]))
43
+ # / clamp_min(sum(rate(urlcode_requests_total[5m])), 0.001) > 0.05
44
+ expr: |
45
+ sum by (route) (rate(urlcode_requests_total{status=~"5.."}[5m]))
46
+ / clamp_min(sum by (route) (rate(urlcode_requests_total[5m])), 0.001) > 0.05
47
+ for: 10m
48
+ labels:
49
+ severity: page
50
+ annotations:
51
+ summary: "Over 5% of requests to {{ $labels.route }} are failing"
52
+ runbook: "502 is a function error, 503 is capacity or a dead pool, 504 is a deadline."
53
+
54
+ - alert: UrlcodeCapacityShedding
55
+ # 503 specifically means admission or the function pool refused work.
56
+ # Built-in endpoint: sum(rate(urlcode_shed_total[5m])) > 1
57
+ expr: sum(rate(urlcode_requests_total{status="503"}[5m])) > 1
58
+ for: 10m
59
+ labels:
60
+ severity: ticket
61
+ annotations:
62
+ summary: "URLCode is shedding requests"
63
+ runbook: "Raise --max-in-flight or --workers, or find what is occupying the pool."
64
+
65
+ - alert: UrlcodeWorkerRestartLoop
66
+ # Replacement backs off but never stops, so a persistent cause shows up
67
+ # as a steady restart rate rather than a stopped pool.
68
+ # Built-in endpoint: urlcode_function_worker_restarts_total and
69
+ # urlcode_link_store_worker_restarts_total, summed.
70
+ expr: sum(rate(urlcode_worker_restarts_total[15m])) > 0
71
+ for: 15m
72
+ labels:
73
+ severity: ticket
74
+ annotations:
75
+ summary: "URLCode workers have been restarting for 15 minutes"
76
+ runbook: "A function is exceeding its deadline or exhausting its heap on real traffic."
77
+
78
+ - alert: UrlcodeLogsDropped
79
+ # The logger drops records when the collector cannot keep up, so the
80
+ # rules above quietly lose their input. The built-in endpoint exports
81
+ # the same name, so this rule works unchanged with either source.
82
+ expr: sum(rate(urlcode_logs_dropped_total[15m])) > 0
83
+ for: 15m
84
+ labels:
85
+ severity: ticket
86
+ annotations:
87
+ summary: "URLCode is dropping log records"
88
+ runbook: "The collector is behind or its sink failed; these alerts are blind until it recovers."
@@ -0,0 +1,20 @@
1
+ # Prometheus scrape job for URLCode's built-in metrics endpoint.
2
+ #
3
+ # The endpoint exists only when the operator starts the server with
4
+ # startServer({ metrics: true }); otherwise /_urlcode/metrics answers 404. It
5
+ # shares the probes' admission budget (--max-in-flight-health) and bind host,
6
+ # is unauthenticated, and discloses route patterns and traffic shape: keep it
7
+ # on an internal interface or restrict it at the ingress. Never expose it
8
+ # publicly.
9
+ #
10
+ # Metric names and labels are fixed in docs/OBSERVABILITY.md. Requests are
11
+ # labelled by status_class (2xx..5xx), not exact status, so the example rules
12
+ # in prometheus-rules.yaml carry a status_class form of each expression next
13
+ # to the log-derived one from vector.toml.
14
+ scrape_configs:
15
+ - job_name: urlcode
16
+ metrics_path: /_urlcode/metrics
17
+ scrape_interval: 15s
18
+ scrape_timeout: 5s
19
+ static_configs:
20
+ - targets: ['urlcode.internal:3000']
@@ -0,0 +1,65 @@
1
+ # Turn URLCode's JSON request log into the counters the example alert rules use.
2
+ #
3
+ # URLCode writes one JSON object per line to stdout. Its built-in endpoint
4
+ # (prometheus-scrape.yaml) is opt-in and labels by status class only; this
5
+ # pipeline keeps the exact status, at the cost of running a collector. Point your
6
+ # supervisor's stdout at Vector (or adapt this to Fluent Bit, Promtail or
7
+ # Alloy — the field names are what matter).
8
+
9
+ [sources.urlcode]
10
+ type = "file"
11
+ include = ["/var/log/urlcode/*.log"]
12
+
13
+ [transforms.parsed]
14
+ type = "remap"
15
+ inputs = ["urlcode"]
16
+ source = '''
17
+ . = parse_json!(string!(.message))
18
+ # Records without an event field are not ours; drop rather than mislabel them.
19
+ if !exists(.event) { abort }
20
+ '''
21
+
22
+ [transforms.requests]
23
+ type = "filter"
24
+ inputs = ["parsed"]
25
+ condition = '.event == "request"'
26
+
27
+ [transforms.workers]
28
+ type = "filter"
29
+ inputs = ["parsed"]
30
+ # Both pools report replacement the same way, so one counter covers them.
31
+ condition = '(.event == "function_worker" || .event == "link_store_worker") && .status == "restarting"'
32
+
33
+ [transforms.dropped]
34
+ type = "filter"
35
+ inputs = ["parsed"]
36
+ condition = '.event == "logs_dropped"'
37
+
38
+ [transforms.metrics]
39
+ type = "log_to_metric"
40
+ inputs = ["requests", "workers", "dropped"]
41
+
42
+ # route is present only with --request-log detailed; it is the configured route
43
+ # pattern, never the requested path, so it is safe as a metric label.
44
+ [[transforms.metrics.metrics]]
45
+ type = "counter"
46
+ field = "status"
47
+ name = "urlcode_requests_total"
48
+ namespace = ""
49
+ tags.status = "{{status}}"
50
+ tags.route = "{{route}}"
51
+
52
+ [[transforms.metrics.metrics]]
53
+ type = "counter"
54
+ field = "status"
55
+ name = "urlcode_worker_restarts_total"
56
+
57
+ [[transforms.metrics.metrics]]
58
+ type = "counter"
59
+ field = "count"
60
+ name = "urlcode_logs_dropped_total"
61
+
62
+ [sinks.prometheus]
63
+ type = "prometheus_exporter"
64
+ inputs = ["metrics"]
65
+ address = "127.0.0.1:9598"
@@ -0,0 +1,48 @@
1
+ # Prerender a dynamic project into a native static one
2
+
3
+ Three pages rendered by a function and one shared template middleware, then
4
+ rendered once at build time into a project that serves the same bytes with
5
+ native `page` routes and no sandbox on the request path.
6
+
7
+ From the runtime checkout:
8
+
9
+ ```sh
10
+ node src/cli.ts dev --project examples/prerender # the dynamic source, live
11
+ node src/cli.ts test --project examples/prerender # 7 fixtures
12
+ node examples/prerender/prerender.mjs examples/prerender /absolute/out
13
+ node src/cli.ts test --project /absolute/out # fixtures the build wrote
14
+ node src/cli.ts audit --project /absolute/out --expect-routes 3
15
+ ```
16
+
17
+ The same three URLs answer identically before and after. The difference is what
18
+ runs to serve them: the source project executes a QuickJS/WASM guest per request,
19
+ the generated project reads a prevalidated byte buffer.
20
+
21
+ | | Source project | Generated project |
22
+ |---|---|---|
23
+ | Handlers | 3 × `function` | 3 × `page` |
24
+ | Middleware | shared template | none |
25
+ | Guest execution per request | yes | none |
26
+ | Content | reviewed literals in YAML | rendered HTML files |
27
+
28
+ `prerender.mjs` is operator build tooling that runs in Node, not guest code. The
29
+ orchestration lives in the runtime's build helper:
30
+
31
+ ```js
32
+ import {prerenderPages, assertNativeProject} from '@jimhoyd/urlcode/prerender';
33
+ ```
34
+
35
+ `prerenderPages` activates the source project, renders each page through its
36
+ middleware, validates every response, enforces the budgets, derives safe
37
+ filenames, writes the files and closes the runtime. It fails the build rather
38
+ than publishing a bad page. What is left in this recipe is the part every site
39
+ does differently: assembling a project from the returned `pages` and `fixtures`,
40
+ then calling `assertNativeProject` to prove the artifact is inert. A larger site
41
+ assembles differently — its own static and download routes, response security
42
+ headers and a generated include — using the same helper.
43
+
44
+ The recipe renders content that is already prepared. It is not a Markdown
45
+ compiler, an HTML sanitizer, an asset pipeline or an incremental build, and it
46
+ copies no static tree. [Prerendering](../../docs/PRERENDER.md) documents the
47
+ helper, its options and guarantees, the limits and how a larger site generates
48
+ its source project.
@@ -0,0 +1,6 @@
1
+ // One page's content, supplied as reviewed literal arguments in urlcode.yaml.
2
+ // This runs in the QuickJS/WASM sandbox with no filesystem, network or host
3
+ // code, at build time exactly as it would at request time.
4
+ export function page(request, {args}) {
5
+ return new Response(args.body, {headers: {'content-type': 'text/html; charset=utf-8'}});
6
+ }
@@ -0,0 +1,29 @@
1
+ // The shared site template. It wraps a function response, whose body is
2
+ // readable through text(); a native page/static/download body is deliberately
3
+ // opaque, which is why the template is applied here, before prerendering, and
4
+ // never to the generated files.
5
+ const escape = value => String(value).replace(/[&<>"]/g, character => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[character]));
6
+ export async function template(request, context, next) {
7
+ const response = await next();
8
+ // Never wrap a failed render: the build must see the original status and stop.
9
+ if (!response.ok) return response;
10
+ const content = await response.text();
11
+ const title = escape(context.args.title);
12
+ return new Response(`<!doctype html>
13
+ <html lang="en">
14
+ <head>
15
+ <meta charset="utf-8">
16
+ <meta name="viewport" content="width=device-width, initial-scale=1">
17
+ <title>${title}</title>
18
+ </head>
19
+ <body>
20
+ <header><a href="/">URLCode prerender example</a></header>
21
+ <main>
22
+ <h1>${title}</h1>
23
+ ${content}
24
+ </main>
25
+ <footer>Prerendered at build time. No guest code runs to serve this page.</footer>
26
+ </body>
27
+ </html>
28
+ `, {status: response.status, headers: {'content-type': 'text/html; charset=utf-8'}});
29
+ }
@@ -0,0 +1,4 @@
1
+ // Declarations for the recipe, so a TypeScript build script (or the test
2
+ // suite) can import it without a cast. The runtime is prerender.mjs.
3
+ export interface PrerenderReport { pages: number; bytes: number; output: string }
4
+ export function prerender(project: string, output: string, options?: { log?: (event: object) => void }): Promise<PrerenderReport>;