@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,17 @@
1
+ id: cookbook
2
+ description: Forty runnable routes covering every handler, middleware, inputs, body validation, headers, assets, includes, host policies and site conventions; route-index.json tags each one.
3
+ tags: [cookbook, reference, routes, handlers, middleware, policies, site, includes, parameters, cookies, methods, expiry, assets, everything]
4
+ complexity: advanced
5
+ capabilities: [bindings, download, enabled, expires, function, methods, middleware, page, parameters, policies.agents, policies.cache, policies.security, policies.throttle, redirect, request.body, respond, response.headers, static]
6
+ targets: {self-hosted: compatible, aws: refused, vercel: refused, cloudflare: refused}
7
+ routes: 40
8
+ files: [urlcode.yaml, routes/code.yaml, routes/redirects.yaml, routes/responses.yaml, routes/files.yaml, routes/policies.yaml, routes/middleware.yaml, route-index.json, tests/requests.json, README.md]
9
+ tests:
10
+ fixtures: tests/requests.json
11
+ commands:
12
+ - node src/cli.ts validate --local --project examples/cookbook
13
+ - node src/cli.ts test --project examples/cookbook
14
+ - node src/cli.ts audit --project examples/cookbook --expect-routes 40
15
+ behavior:
16
+ - urlcode examples search finds single routes here through the generated route-index.json
17
+ - fifteen routes exercise the fourteen middleware modules that the middleware recipe copies
@@ -0,0 +1,3 @@
1
+ export default function catalog() {
2
+ return Response.json({version: 3, items: ['alpha', 'beta']});
3
+ }
@@ -0,0 +1,4 @@
1
+ export default function fail(request, {args}) {
2
+ if (args.fail) throw new Error('simulated failure');
3
+ return Response.json({healthy: true});
4
+ }
@@ -0,0 +1,3 @@
1
+ export default function items() {
2
+ return Response.json([{id: 1, name: 'first'}, {id: 2, name: 'second'}]);
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function profile(request, {state}) {
2
+ return Response.json({welcome: state.body.name, age: state.body.age ?? null}, {status: 201});
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function resource(request, {state}) {
2
+ return Response.json({method: state.method, tunneled: state.method !== request.method});
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function status() {
2
+ return Response.json({service: 'cookbook', state: 'ok'});
3
+ }
@@ -0,0 +1,48 @@
1
+ // Authentication gates. Like Hono bearerAuth/basicAuth or express-basic-auth,
2
+ // but the expected credential comes from the route's declared bindings.
3
+ // The cookbook binds literal values; real projects use {secret: name} grants.
4
+
5
+ // Compare without leaking where two strings differ through timing.
6
+ function same(actual, expected) {
7
+ if (typeof actual !== 'string' || typeof expected !== 'string') return false;
8
+ const length = Math.max(actual.length, expected.length);
9
+ let mismatch = actual.length ^ expected.length;
10
+ for (let i = 0; i < length; i++) mismatch |= (actual.charCodeAt(i) || 0) ^ (expected.charCodeAt(i) || 0);
11
+ return mismatch === 0 && length > 0;
12
+ }
13
+
14
+ // The guest exposes no atob; a small decoder handles standard Basic credentials.
15
+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
16
+ function decodeBase64(text) {
17
+ const clean = String(text).replace(/=+$/, '');
18
+ if (!/^[A-Za-z0-9+/]*$/.test(clean)) return null;
19
+ let bits = 0, buffer = 0, out = '';
20
+ for (const char of clean) {
21
+ buffer = (buffer << 6) | alphabet.indexOf(char); bits += 6;
22
+ if (bits >= 8) { bits -= 8; out += String.fromCharCode((buffer >> bits) & 0xff); }
23
+ }
24
+ return out;
25
+ }
26
+
27
+ export async function bearer(request, context, next) {
28
+ const header = request.headers.get('authorization') || '';
29
+ const token = header.startsWith('Bearer ') ? header.slice(7).trim() : '';
30
+ if (!same(token, context.env.API_TOKEN)) {
31
+ return new Response('Unauthorized\n', {status: 401, headers: {'www-authenticate': 'Bearer realm="cookbook"', 'cache-control': 'no-store'}});
32
+ }
33
+ return next();
34
+ }
35
+
36
+ export async function basic(request, context, next) {
37
+ const header = request.headers.get('authorization') || '';
38
+ const decoded = header.startsWith('Basic ') ? decodeBase64(header.slice(6).trim()) : null;
39
+ const separator = decoded === null ? -1 : decoded.indexOf(':');
40
+ const user = separator < 0 ? '' : decoded.slice(0, separator), password = separator < 0 ? '' : decoded.slice(separator + 1);
41
+ // Evaluate both comparisons so a wrong user costs the same as a wrong password.
42
+ const userOk = same(user, context.env.ADMIN_USER), passwordOk = same(password, context.env.ADMIN_PASSWORD);
43
+ if (!(userOk && passwordOk)) {
44
+ return new Response('Unauthorized\n', {status: 401, headers: {'www-authenticate': 'Basic realm="cookbook", charset="UTF-8"', 'cache-control': 'no-store'}});
45
+ }
46
+ context.state.user = user;
47
+ return next();
48
+ }
@@ -0,0 +1,15 @@
1
+ // Validate a JSON body once and hand the parsed object to the handler through
2
+ // context.state. Bodies are single-use, so downstream code must not read again.
3
+ export default async function validateBody(request, context, next) {
4
+ const body = await request.json();
5
+ const errors = [];
6
+ if (typeof body !== 'object' || body === null || Array.isArray(body)) errors.push('body must be an object');
7
+ else {
8
+ if (typeof body.name !== 'string' || body.name.length < 1 || body.name.length > 80) errors.push('name must be 1-80 characters');
9
+ if (body.age !== undefined && (!Number.isInteger(body.age) || body.age < 0 || body.age > 150)) errors.push('age must be an integer from 0 to 150');
10
+ for (const key of Object.keys(body)) if (!['name', 'age'].includes(key)) errors.push('unknown field ' + key);
11
+ }
12
+ if (errors.length) return Response.json({errors}, {status: 422});
13
+ context.state.body = body;
14
+ return next();
15
+ }
@@ -0,0 +1,19 @@
1
+ // Sticky A/B bucketing through a cookie, as on Vercel or Cloudflare edge examples.
2
+ // Bucket b gets a different destination; everyone keeps their bucket for a week.
3
+ function cookie(request, name) {
4
+ for (const part of (request.headers.get('cookie') || '').split(';')) {
5
+ const [key, ...rest] = part.trim().split('=');
6
+ if (key === name) return rest.join('=');
7
+ }
8
+ return null;
9
+ }
10
+ export default async function bucket(request, context, next) {
11
+ let assigned = cookie(request, 'bucket');
12
+ const fresh = assigned !== 'a' && assigned !== 'b';
13
+ if (fresh) assigned = Math.random() < 0.5 ? 'a' : 'b';
14
+ context.state.bucket = assigned;
15
+ const response = assigned === 'b' ? Response.redirect(context.env.VARIANT_URL, 302) : await next();
16
+ response.headers.set('vary', 'cookie');
17
+ if (fresh) response.headers.append('set-cookie', 'bucket=' + assigned + '; Path=/; Max-Age=604800; SameSite=Lax');
18
+ return response;
19
+ }
@@ -0,0 +1,21 @@
1
+ // Cross-origin resource sharing with an origin allowlist, as in Express cors
2
+ // or Hono cors. The route must declare OPTIONS in its methods for preflight.
3
+ const preflightHeaders = {
4
+ 'access-control-allow-methods': 'GET, HEAD, OPTIONS',
5
+ 'access-control-allow-headers': 'Content-Type, Authorization',
6
+ 'access-control-max-age': '600'
7
+ };
8
+ export default async function cors(request, context, next) {
9
+ const allowed = (context.env.ALLOWED_ORIGINS || '').split(/\s+/).filter(Boolean);
10
+ const origin = request.headers.get('origin');
11
+ const permitted = origin !== null && allowed.includes(origin);
12
+ if (request.method === 'OPTIONS') {
13
+ // Preflight never reaches the handler. Unknown origins get no allow headers.
14
+ const headers = {vary: 'origin', ...(permitted ? {...preflightHeaders, 'access-control-allow-origin': origin} : {})};
15
+ return new Response(null, {status: 204, headers});
16
+ }
17
+ const response = await next();
18
+ response.headers.set('vary', 'origin');
19
+ if (permitted) response.headers.set('access-control-allow-origin', origin);
20
+ return response;
21
+ }
@@ -0,0 +1,13 @@
1
+ // Authoring aid: with DEBUG on and X-Debug: 1, describe what the guest received.
2
+ // Secrets and credential-bearing headers are never echoed.
3
+ const hidden = ['authorization', 'cookie', 'proxy-authorization'];
4
+ export default async function debug(request, context, next) {
5
+ if (context.env.DEBUG !== 'true' || request.headers.get('x-debug') !== '1') return next();
6
+ const headers = {};
7
+ for (const [name, value] of request.headers) headers[name] = hidden.includes(name) ? '[redacted]' : value;
8
+ return Response.json({
9
+ method: request.method, url: request.url, headers,
10
+ inputs: context.inputs, args: context.args ?? null,
11
+ env: Object.keys(context.env), secrets: Object.keys(context.secrets)
12
+ }, {headers: {'cache-control': 'no-store'}});
13
+ }
@@ -0,0 +1,11 @@
1
+ // Wrap successful JSON function responses in a stable envelope. Native bodies are
2
+ // opaque and pass through untouched; only readable JSON is rewritten.
3
+ export default async function envelope(request, context, next) {
4
+ const response = await next();
5
+ if (!response.ok || !(response.headers.get('content-type') || '').startsWith('application/json')) return response;
6
+ let data;
7
+ try { data = await response.json(); } catch { return response; }
8
+ const path = request.url.replace(/^[a-z]+:\/\/[^/]+/i, '').split('?')[0];
9
+ const meta = {route: path, count: Array.isArray(data) ? data.length : 1};
10
+ return Response.json({ok: true, data, meta}, {status: response.status, headers: response.headers});
11
+ }
@@ -0,0 +1,11 @@
1
+ // Error boundary. Without it, a throwing function is a bare 502 from the runtime.
2
+ // Nothing about the thrown error reaches the client; the guest console is silent,
3
+ // so the correlation id is what an operator can search for.
4
+ export default async function errorBoundary(request, context, next) {
5
+ try {
6
+ return await next();
7
+ } catch {
8
+ return Response.json({error: 'Temporarily unavailable', correlationId: context.state.correlationId ?? null},
9
+ {status: 500, headers: {'cache-control': 'no-store'}});
10
+ }
11
+ }
@@ -0,0 +1,18 @@
1
+ // Conditional requests for function output: a weak ETag from a cheap FNV-1a
2
+ // hash, plus 304 when the client already holds that version. Native static
3
+ // and download routes already do this in the runtime; this is for JSON.
4
+ function fnv1a(text) {
5
+ let hash = 0x811c9dc5;
6
+ for (let i = 0; i < text.length; i++) { hash ^= text.charCodeAt(i); hash = Math.imul(hash, 0x01000193) >>> 0; }
7
+ return hash.toString(16).padStart(8, '0');
8
+ }
9
+ export default async function etag(request, context, next) {
10
+ const response = await next();
11
+ if (!response.ok || request.method !== 'GET' && request.method !== 'HEAD') return response;
12
+ const body = await response.text();
13
+ const tag = 'W/"' + fnv1a(body) + '"';
14
+ const headers = {...Object.fromEntries(response.headers), etag: tag, 'cache-control': 'public, max-age=60'};
15
+ const held = (request.headers.get('if-none-match') || '').split(',').map(s => s.trim());
16
+ if (held.includes(tag) || held.includes('*')) return new Response(null, {status: 304, headers: {etag: tag, 'cache-control': headers['cache-control']}});
17
+ return new Response(body, {status: response.status, headers});
18
+ }
@@ -0,0 +1,16 @@
1
+ // Language redirect from Accept-Language, like Next.js i18n middleware. Only
2
+ // languages listed in LOCALES are chosen; the native redirect is the default.
3
+ export default async function locale(request, context, next) {
4
+ const supported = (context.env.LOCALES || '').split(/\s+/).filter(Boolean);
5
+ const ranked = (request.headers.get('accept-language') || '').split(',').map((part, index) => {
6
+ const [tag, ...params] = part.trim().split(';');
7
+ const q = params.map(p => p.trim()).find(p => p.startsWith('q='));
8
+ return {lang: tag.trim().toLowerCase().split('-')[0], q: q ? Number(q.slice(2)) || 0 : 1, index};
9
+ }).filter(p => p.q > 0).sort((a, b) => b.q - a.q || a.index - b.index);
10
+ const chosen = ranked.find(p => supported.includes(p.lang))?.lang;
11
+ const response = chosen && chosen !== supported[0]
12
+ ? Response.redirect(context.env.SITE + '/' + chosen + '/welcome', 302)
13
+ : await next();
14
+ response.headers.set('vary', 'accept-language');
15
+ return response;
16
+ }
@@ -0,0 +1,10 @@
1
+ // A kill switch: return 503 while MAINTENANCE is on, unless the caller presents
2
+ // the bypass token. Flip the binding and restart; no code change is needed.
3
+ export default async function maintenance(request, context, next) {
4
+ const active = context.env.MAINTENANCE === 'true';
5
+ const bypass = context.env.MAINTENANCE_BYPASS;
6
+ if (active && !(bypass && request.headers.get('x-maintenance-bypass') === bypass)) {
7
+ return new Response('Down for maintenance\n', {status: 503, headers: {'retry-after': '120', 'cache-control': 'no-store'}});
8
+ }
9
+ return next();
10
+ }
@@ -0,0 +1,15 @@
1
+ // Method override for clients that can only send POST (Express method-override).
2
+ // The route still declares every real method; only a bounded set may be tunneled.
3
+ const tunnelable = ['PUT', 'PATCH', 'DELETE'];
4
+ export async function override(request, context, next) {
5
+ const requested = (request.headers.get('x-http-method-override') || '').toUpperCase();
6
+ if (request.method === 'POST' && requested) {
7
+ if (!tunnelable.includes(requested)) {
8
+ return new Response('Method override not allowed\n', {status: 405, headers: {allow: tunnelable.join(', ')}});
9
+ }
10
+ context.state.method = requested;
11
+ } else {
12
+ context.state.method = request.method;
13
+ }
14
+ return next();
15
+ }
@@ -0,0 +1,20 @@
1
+ // Content negotiation in the spirit of Express res.format. The handler always
2
+ // produces JSON; this converts it to plain text when the client prefers that.
3
+ function preferences(accept) {
4
+ return (accept || '*/*').split(',').map((part, index) => {
5
+ const [type, ...params] = part.trim().split(';');
6
+ const q = params.map(p => p.trim()).find(p => p.startsWith('q='));
7
+ return {type: type.trim().toLowerCase(), q: q ? Number(q.slice(2)) || 0 : 1, index};
8
+ }).filter(p => p.q > 0).sort((a, b) => b.q - a.q || a.index - b.index);
9
+ }
10
+ export default async function negotiate(request, context, next) {
11
+ const ranked = preferences(request.headers.get('accept'));
12
+ const choice = ranked.find(p => ['application/json', 'text/plain', 'application/*', 'text/*', '*/*'].includes(p.type));
13
+ if (!choice) return new Response('Not acceptable: application/json or text/plain\n', {status: 406});
14
+ const response = await next();
15
+ response.headers.set('vary', 'accept');
16
+ if (!response.ok || !['text/plain', 'text/*'].includes(choice.type)) return response;
17
+ const value = await response.json();
18
+ const lines = Object.entries(value).map(([key, item]) => key + ': ' + String(item)).join('\n') + '\n';
19
+ return new Response(lines, {status: response.status, headers: {...Object.fromEntries(response.headers), 'content-type': 'text/plain; charset=utf-8'}});
20
+ }
@@ -0,0 +1,12 @@
1
+ // Hotlink protection for a download: the Referer host must be on the allowlist.
2
+ // Referers are advisory, so this deters casual embedding rather than securing anything.
3
+ function host(referer) {
4
+ const match = /^https?:\/\/([^/:?#]+)/i.exec(referer || '');
5
+ return match ? match[1].toLowerCase() : null;
6
+ }
7
+ export default async function referer(request, context, next) {
8
+ const allowed = (context.env.ALLOWED_REFERERS || '').split(/\s+/).filter(Boolean);
9
+ const from = host(request.headers.get('referer'));
10
+ if (from === null || !allowed.includes(from)) return new Response('Direct download links are not permitted\n', {status: 403});
11
+ return next();
12
+ }
@@ -0,0 +1,16 @@
1
+ // Correlation id and timing headers (Hono requestId/timing, Express response-time).
2
+ // The runtime already stamps its own x-request-id on every reply; this carries the
3
+ // caller's id instead so a client can match its logs to yours. The guest has no
4
+ // crypto, so a generated id is a trace label, never a secret.
5
+ const validId = /^[A-Za-z0-9._-]{1,64}$/;
6
+ export default async function correlation(request, context, next) {
7
+ const started = Date.now();
8
+ const incoming = request.headers.get('x-correlation-id');
9
+ context.state.correlationId = incoming !== null && validId.test(incoming)
10
+ ? incoming
11
+ : started.toString(36) + '-' + Math.floor(Math.random() * 0xffffffff).toString(36);
12
+ const response = await next();
13
+ response.headers.set('x-correlation-id', context.state.correlationId);
14
+ response.headers.set('server-timing', 'app;dur=' + (Date.now() - started));
15
+ return response;
16
+ }