@jimhoyd/urlcode 0.3.0 → 0.4.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (380) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +122 -0
  2. package/.claude/skills/urlcode-operations/SKILL.md +108 -0
  3. package/.claude-plugin/marketplace.json +18 -0
  4. package/CONTRIBUTING.md +30 -2
  5. package/README.md +195 -255
  6. package/ROADMAP.md +143 -15
  7. package/SECURITY.md +31 -9
  8. package/dist/BUILD-MANIFEST.json +72 -47
  9. package/dist/adapters.js +4 -23
  10. package/dist/agent-lists.js +1 -1
  11. package/dist/agents-guide.js +113 -0
  12. package/dist/authoring-files.js +60 -0
  13. package/dist/authoring.js +11 -1
  14. package/dist/aws.js +4 -3
  15. package/dist/build-cloudflare.js +11 -24
  16. package/dist/build-static.js +134 -0
  17. package/dist/bulk.js +37 -0
  18. package/dist/capabilities.js +262 -0
  19. package/dist/capability-query.js +71 -0
  20. package/dist/catalog.js +105 -0
  21. package/dist/cli.js +165 -34
  22. package/dist/client-address.js +1 -1
  23. package/dist/compliance-rules/baseline.js +9 -17
  24. package/dist/compliance-rules/privacy.js +7 -18
  25. package/dist/compliance-rules/shared.js +0 -2
  26. package/dist/compliance-rules/strict.js +5 -5
  27. package/dist/compliance.js +6 -8
  28. package/dist/conditions.js +88 -0
  29. package/dist/config.js +69 -6
  30. package/dist/context.js +155 -0
  31. package/dist/ecosystem-cli.js +88 -0
  32. package/dist/egress.js +98 -0
  33. package/dist/examples.js +92 -0
  34. package/dist/explain-cli.js +64 -0
  35. package/dist/explain.js +131 -0
  36. package/dist/extensions.js +231 -0
  37. package/dist/function-sources.js +49 -5
  38. package/dist/function-worker.js +3 -1
  39. package/dist/functions.js +84 -13
  40. package/dist/guest-api.js +29 -3
  41. package/dist/index.js +40 -6
  42. package/dist/init-with.js +165 -0
  43. package/dist/interchange-cli.js +42 -0
  44. package/dist/interchange.js +189 -0
  45. package/dist/manifest.js +109 -0
  46. package/dist/match.js +2 -2
  47. package/dist/mcp-authoring.js +147 -0
  48. package/dist/mcp.js +97 -0
  49. package/dist/observability.js +7 -21
  50. package/dist/operator-host.js +29 -0
  51. package/dist/plugins.js +12 -0
  52. package/dist/policies/agents.js +2 -2
  53. package/dist/policies/cache.js +8 -3
  54. package/dist/policies/compression.js +2 -1
  55. package/dist/policies/security.js +0 -0
  56. package/dist/policies.js +1 -1
  57. package/dist/policy.js +56 -15
  58. package/dist/prerender.js +100 -41
  59. package/dist/project-tests.js +3 -3
  60. package/dist/provider-verification.js +92 -0
  61. package/dist/proxy.js +44 -0
  62. package/dist/readiness.js +34 -11
  63. package/dist/recipes.js +41 -0
  64. package/dist/route-diff.js +106 -0
  65. package/dist/router.js +45 -7
  66. package/dist/runtime.js +164 -64
  67. package/dist/sandbox.js +48 -0
  68. package/dist/scaffold.js +0 -0
  69. package/dist/schema-query.js +62 -0
  70. package/dist/scripts/operational-drills.js +12 -54
  71. package/dist/server.js +3 -29
  72. package/dist/signals.js +24 -0
  73. package/dist/site.js +0 -0
  74. package/dist/tooling.js +96 -0
  75. package/dist/trusted-functions.js +210 -0
  76. package/dist/types/adapters.d.ts +7 -4
  77. package/dist/types/agent-lists.d.ts +0 -1
  78. package/dist/types/agents-guide.d.ts +17 -0
  79. package/dist/types/authoring-files.d.ts +10 -0
  80. package/dist/types/aws.d.ts +3 -1
  81. package/dist/types/build-cloudflare.d.ts +1 -0
  82. package/dist/types/build-static.d.ts +43 -0
  83. package/dist/types/bulk.d.ts +27 -0
  84. package/dist/types/capabilities.d.ts +64 -0
  85. package/dist/types/capability-query.d.ts +24 -0
  86. package/dist/types/catalog.d.ts +65 -0
  87. package/dist/types/client-address.d.ts +0 -1
  88. package/dist/types/compliance-rules/baseline.d.ts +1 -9
  89. package/dist/types/compliance-rules/privacy.d.ts +1 -4
  90. package/dist/types/compliance-rules/shared.d.ts +0 -2
  91. package/dist/types/compliance-rules/strict.d.ts +0 -5
  92. package/dist/types/compliance.d.ts +0 -3
  93. package/dist/types/conditions.d.ts +19 -0
  94. package/dist/types/config.d.ts +21 -2
  95. package/dist/types/context.d.ts +66 -0
  96. package/dist/types/ecosystem-cli.d.ts +17 -0
  97. package/dist/types/egress.d.ts +46 -0
  98. package/dist/types/examples.d.ts +50 -0
  99. package/dist/types/explain-cli.d.ts +11 -0
  100. package/dist/types/explain.d.ts +95 -0
  101. package/dist/types/extensions.d.ts +177 -0
  102. package/dist/types/function-sources.d.ts +9 -0
  103. package/dist/types/functions.d.ts +48 -5
  104. package/dist/types/guest-api.d.ts +1 -0
  105. package/dist/types/index.d.ts +36 -6
  106. package/dist/types/init-with.d.ts +30 -0
  107. package/dist/types/interchange-cli.d.ts +16 -0
  108. package/dist/types/interchange.d.ts +42 -0
  109. package/dist/types/manifest.d.ts +79 -0
  110. package/dist/types/match.d.ts +1 -0
  111. package/dist/types/mcp-authoring.d.ts +92 -0
  112. package/dist/types/mcp.d.ts +12 -0
  113. package/dist/types/observability.d.ts +3 -14
  114. package/dist/types/operator-host.d.ts +8 -0
  115. package/dist/types/plugins.d.ts +2 -0
  116. package/dist/types/policies/agents.d.ts +0 -2
  117. package/dist/types/policies/compression.d.ts +2 -0
  118. package/dist/types/policies/security.d.ts +0 -1
  119. package/dist/types/policy.d.ts +15 -4
  120. package/dist/types/project-tests.d.ts +3 -2
  121. package/dist/types/provider-verification.d.ts +53 -0
  122. package/dist/types/proxy.d.ts +21 -0
  123. package/dist/types/readiness.d.ts +10 -3
  124. package/dist/types/recipes.d.ts +30 -0
  125. package/dist/types/route-diff.d.ts +27 -0
  126. package/dist/types/router.d.ts +2 -1
  127. package/dist/types/runtime.d.ts +11 -27
  128. package/dist/types/sandbox.d.ts +12 -0
  129. package/dist/types/scaffold.d.ts +0 -2
  130. package/dist/types/schema-query.d.ts +12 -0
  131. package/dist/types/server.d.ts +1 -4
  132. package/dist/types/signals.d.ts +25 -0
  133. package/dist/types/site.d.ts +0 -1
  134. package/dist/types/tooling.d.ts +115 -0
  135. package/dist/types/trusted-functions.d.ts +29 -0
  136. package/dist/types/types.d.ts +71 -7
  137. package/dist/types/typescript-authoring.d.ts +12 -0
  138. package/dist/types/vercel.d.ts +3 -1
  139. package/dist/types/verify-deployment.d.ts +47 -0
  140. package/dist/types.js +37 -5
  141. package/dist/typescript-authoring.js +142 -0
  142. package/dist/vercel.js +4 -3
  143. package/dist/verify-deployment.js +270 -0
  144. package/docs/AI-AUTHORING.md +232 -15
  145. package/docs/AWS.md +4 -4
  146. package/docs/BEST-PRACTICES.md +3 -2
  147. package/docs/BULK.md +79 -0
  148. package/docs/CAPABILITIES.md +192 -0
  149. package/docs/CAPACITY.md +129 -32
  150. package/docs/CI.md +142 -0
  151. package/docs/CLOUDFLARE.md +1 -2
  152. package/docs/COMPLIANCE.md +6 -9
  153. package/docs/CONDITIONS.md +74 -0
  154. package/docs/DEPLOYMENT-CHECKS.md +108 -0
  155. package/docs/EGRESS.md +125 -0
  156. package/docs/EXTENSIONS.md +398 -0
  157. package/docs/FRAMEWORK.md +198 -0
  158. package/docs/FUNCTION-SECURITY.md +129 -32
  159. package/docs/INSTALL.md +45 -12
  160. package/docs/INTERCHANGE.md +134 -0
  161. package/docs/LOAD-TESTING.md +4 -4
  162. package/docs/MIDDLEWARE-EXAMPLES.md +75 -0
  163. package/docs/MIDDLEWARE.md +29 -16
  164. package/docs/MONITORING.md +2 -19
  165. package/docs/NEXT-PHASE-PLAN.md +98 -0
  166. package/docs/NEXT-STEPS.md +634 -0
  167. package/docs/OBSERVABILITY.md +11 -18
  168. package/docs/OPEN-DECISIONS.md +212 -0
  169. package/docs/OPERATIONAL-PROOF.md +30 -31
  170. package/docs/OPERATIONS.md +29 -35
  171. package/docs/PLUGINS.md +37 -0
  172. package/docs/POLICIES.md +23 -309
  173. package/docs/PRERENDER.md +41 -1
  174. package/docs/PROJECT-DIRECTION.md +75 -8
  175. package/docs/PROVIDER-VERIFICATION.md +84 -0
  176. package/docs/READINESS.md +21 -1
  177. package/docs/README.md +87 -34
  178. package/docs/RECIPES.md +99 -0
  179. package/docs/RELEASE-READINESS.md +57 -35
  180. package/docs/RELEASE-SECURITY.md +116 -7
  181. package/docs/RESILIENCE.md +16 -15
  182. package/docs/ROUTING.md +8 -10
  183. package/docs/SANDBOX-REVIEW.md +19 -6
  184. package/docs/SCAFFOLDING.md +0 -2
  185. package/docs/SECURITY-AUDIT.md +41 -1
  186. package/docs/SPECIFICATION.md +150 -29
  187. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +287 -0
  188. package/docs/SPIKE-BUSINESS-SUITE.md +1021 -0
  189. package/docs/SPIKE-CORE-LAYERING.md +337 -0
  190. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +209 -0
  191. package/docs/SPIKE-EXTENSION-MODEL.md +419 -0
  192. package/docs/SPIKE-EXTENSIONS.md +6 -0
  193. package/docs/SPIKE-LAMBDA-COMPILE.md +201 -0
  194. package/docs/SPIKE-MONOREPO.md +322 -0
  195. package/docs/STANDARDS.md +150 -142
  196. package/docs/STARTERS.md +21 -1
  197. package/docs/STATIC.md +94 -0
  198. package/docs/TOOLING.md +295 -0
  199. package/docs/TUNNELS.md +0 -3
  200. package/docs/TYPESCRIPT-AUTHORING.md +82 -0
  201. package/docs/TYPESCRIPT.md +25 -4
  202. package/docs/USABILITY-REVIEW.md +129 -0
  203. package/docs/VERCEL.md +4 -5
  204. package/docs/VERSION-ALIGNMENT.md +205 -0
  205. package/docs/YAML-GUIDE.md +15 -479
  206. package/docs/YAML-REFERENCE.md +143 -22
  207. package/docs/policies/agents.md +1 -1
  208. package/docs/policies/cache.md +13 -0
  209. package/docs/policies/contract.md +52 -0
  210. package/docs/policies/hardened.md +56 -0
  211. package/docs/policies/interoperability.md +169 -0
  212. package/docs/policies/operations.md +45 -0
  213. package/docs/yaml/assets.md +36 -0
  214. package/docs/yaml/conditions.md +20 -0
  215. package/docs/yaml/functions.md +160 -0
  216. package/docs/yaml/middleware.md +29 -0
  217. package/docs/yaml/organization.md +74 -0
  218. package/docs/yaml/policies.md +37 -0
  219. package/docs/yaml/redirects.md +64 -0
  220. package/docs/yaml/responses.md +57 -0
  221. package/docs/yaml/site.md +24 -0
  222. package/examples/assets/example.yaml +17 -0
  223. package/examples/aws/example.yaml +20 -0
  224. package/examples/cloudflare/example.yaml +19 -0
  225. package/examples/compliance/example.yaml +11 -0
  226. package/examples/conditions/README.md +12 -0
  227. package/examples/conditions/example.yaml +19 -0
  228. package/examples/conditions/tests/requests.json +13 -0
  229. package/examples/conditions/urlcode.yaml +24 -0
  230. package/examples/cookbook/README.md +8 -4
  231. package/examples/cookbook/example.yaml +17 -0
  232. package/examples/cookbook/functions/catalog.mjs +3 -0
  233. package/examples/cookbook/functions/fail.mjs +4 -0
  234. package/examples/cookbook/functions/items.mjs +3 -0
  235. package/examples/cookbook/functions/profile.mjs +3 -0
  236. package/examples/cookbook/functions/resource.mjs +3 -0
  237. package/examples/cookbook/functions/status.mjs +3 -0
  238. package/examples/cookbook/middleware/auth.mjs +48 -0
  239. package/examples/cookbook/middleware/body.mjs +15 -0
  240. package/examples/cookbook/middleware/bucket.mjs +29 -0
  241. package/examples/cookbook/middleware/cors.mjs +21 -0
  242. package/examples/cookbook/middleware/debug.mjs +13 -0
  243. package/examples/cookbook/middleware/envelope.mjs +11 -0
  244. package/examples/cookbook/middleware/errors.mjs +11 -0
  245. package/examples/cookbook/middleware/etag.mjs +18 -0
  246. package/examples/cookbook/middleware/locale.mjs +20 -0
  247. package/examples/cookbook/middleware/maintenance.mjs +10 -0
  248. package/examples/cookbook/middleware/methods.mjs +15 -0
  249. package/examples/cookbook/middleware/negotiate.mjs +20 -0
  250. package/examples/cookbook/middleware/referer.mjs +12 -0
  251. package/examples/cookbook/middleware/request-id.mjs +16 -0
  252. package/examples/cookbook/route-index.json +676 -0
  253. package/examples/cookbook/routes/middleware.yaml +126 -0
  254. package/examples/cookbook/tests/requests.json +526 -0
  255. package/examples/cookbook/urlcode.yaml +1 -0
  256. package/examples/egress/README.md +22 -0
  257. package/examples/egress/example.yaml +19 -0
  258. package/examples/egress/urlcode.yaml +19 -0
  259. package/examples/extensions/README.md +7 -0
  260. package/examples/extensions/example.yaml +21 -0
  261. package/examples/extensions/urlcode.yaml +25 -0
  262. package/examples/monitoring/example.yaml +8 -0
  263. package/examples/prerender/README.md +2 -2
  264. package/examples/prerender/example.yaml +16 -0
  265. package/examples/provider-conformance/README.md +12 -0
  266. package/examples/provider-conformance/example.yaml +14 -0
  267. package/examples/provider-conformance/urlcode.yaml +34 -0
  268. package/examples/tunnel/example.yaml +8 -0
  269. package/examples/vercel/example.yaml +19 -0
  270. package/llms-full.txt +3084 -0
  271. package/llms.txt +61 -21
  272. package/package.json +36 -7
  273. package/packaging/claude-plugin/.claude-plugin/plugin.json +19 -0
  274. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +122 -0
  275. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +108 -0
  276. package/recipes/authenticated-json-api/README.md +51 -0
  277. package/recipes/authenticated-json-api/functions/profile.mjs +5 -0
  278. package/recipes/authenticated-json-api/recipe.yaml +34 -0
  279. package/recipes/authenticated-json-api/tests/requests.json +39 -0
  280. package/recipes/authenticated-json-api/urlcode.yaml +12 -0
  281. package/recipes/contact-form/README.md +25 -0
  282. package/recipes/contact-form/functions/contact.mjs +17 -0
  283. package/recipes/contact-form/recipe.yaml +33 -0
  284. package/recipes/contact-form/tests/requests.json +47 -0
  285. package/recipes/contact-form/urlcode.yaml +18 -0
  286. package/recipes/cors-api/README.md +16 -0
  287. package/recipes/cors-api/functions/items.mjs +3 -0
  288. package/recipes/cors-api/middleware/cors.mjs +21 -0
  289. package/recipes/cors-api/recipe.yaml +26 -0
  290. package/recipes/cors-api/tests/requests.json +65 -0
  291. package/recipes/cors-api/urlcode.yaml +12 -0
  292. package/recipes/health-page/README.md +13 -0
  293. package/recipes/health-page/recipe.yaml +23 -0
  294. package/recipes/health-page/tests/requests.json +36 -0
  295. package/recipes/health-page/urlcode.yaml +19 -0
  296. package/recipes/json-api/README.md +6 -0
  297. package/recipes/json-api/functions/echo.mjs +3 -0
  298. package/recipes/json-api/recipe.yaml +25 -0
  299. package/recipes/json-api/tests/requests.json +34 -0
  300. package/recipes/json-api/urlcode.yaml +12 -0
  301. package/recipes/middleware/README.md +34 -0
  302. package/recipes/middleware/functions/catalog.mjs +3 -0
  303. package/recipes/middleware/functions/fail.mjs +4 -0
  304. package/recipes/middleware/functions/items.mjs +3 -0
  305. package/recipes/middleware/functions/profile.mjs +3 -0
  306. package/recipes/middleware/functions/resource.mjs +3 -0
  307. package/recipes/middleware/functions/status.mjs +3 -0
  308. package/recipes/middleware/middleware/auth.mjs +48 -0
  309. package/recipes/middleware/middleware/body.mjs +15 -0
  310. package/recipes/middleware/middleware/bucket.mjs +29 -0
  311. package/recipes/middleware/middleware/cors.mjs +21 -0
  312. package/recipes/middleware/middleware/debug.mjs +13 -0
  313. package/recipes/middleware/middleware/envelope.mjs +11 -0
  314. package/recipes/middleware/middleware/errors.mjs +11 -0
  315. package/recipes/middleware/middleware/etag.mjs +18 -0
  316. package/recipes/middleware/middleware/locale.mjs +20 -0
  317. package/recipes/middleware/middleware/maintenance.mjs +10 -0
  318. package/recipes/middleware/middleware/methods.mjs +15 -0
  319. package/recipes/middleware/middleware/negotiate.mjs +20 -0
  320. package/recipes/middleware/middleware/referer.mjs +12 -0
  321. package/recipes/middleware/middleware/request-id.mjs +16 -0
  322. package/recipes/middleware/public/guide.txt +1 -0
  323. package/recipes/middleware/recipe.yaml +50 -0
  324. package/recipes/middleware/tests/requests.json +528 -0
  325. package/recipes/middleware/urlcode.yaml +127 -0
  326. package/recipes/protected-download/README.md +22 -0
  327. package/recipes/protected-download/files/report.txt +1 -0
  328. package/recipes/protected-download/recipe.yaml +31 -0
  329. package/recipes/protected-download/tests/requests.json +32 -0
  330. package/recipes/protected-download/urlcode.yaml +15 -0
  331. package/recipes/redirect/README.md +7 -0
  332. package/recipes/redirect/recipe.yaml +25 -0
  333. package/recipes/redirect/tests/requests.json +19 -0
  334. package/recipes/redirect/urlcode.yaml +9 -0
  335. package/recipes/static-plus-api/README.md +15 -0
  336. package/recipes/static-plus-api/functions/info.mjs +3 -0
  337. package/recipes/static-plus-api/public/assets/index.html +3 -0
  338. package/recipes/static-plus-api/public/assets/site.css +1 -0
  339. package/recipes/static-plus-api/public/index.html +8 -0
  340. package/recipes/static-plus-api/recipe.yaml +29 -0
  341. package/recipes/static-plus-api/tests/requests.json +56 -0
  342. package/recipes/static-plus-api/urlcode.yaml +23 -0
  343. package/recipes/typescript/README.md +8 -0
  344. package/recipes/typescript/functions/hello.ts +5 -0
  345. package/recipes/typescript/recipe.yaml +23 -0
  346. package/recipes/typescript/tests/requests.json +18 -0
  347. package/recipes/typescript/urlcode.yaml +5 -0
  348. package/recipes/webhook-receiver/README.md +20 -0
  349. package/recipes/webhook-receiver/functions/receive.mjs +16 -0
  350. package/recipes/webhook-receiver/recipe.yaml +27 -0
  351. package/recipes/webhook-receiver/tests/requests.json +59 -0
  352. package/recipes/webhook-receiver/urlcode.yaml +23 -0
  353. package/schemas/recipe.schema.json +139 -0
  354. package/schemas/urlcode.schema.json +659 -110
  355. package/skills/urlcode/SKILL.md +119 -0
  356. package/starters/default/.github/workflows/urlcode.yml +23 -0
  357. package/starters/default/.mcp.json +12 -0
  358. package/starters/default/AGENTS.md +79 -0
  359. package/starters/default/urlcode.yaml +0 -1
  360. package/dist/link-api.js +0 -136
  361. package/dist/link-cli.js +0 -141
  362. package/dist/link-events.js +0 -76
  363. package/dist/link-records.js +0 -31
  364. package/dist/link-store-worker.js +0 -150
  365. package/dist/link-store.js +0 -250
  366. package/dist/management-policy.js +0 -41
  367. package/dist/sqlite-version.js +0 -6
  368. package/dist/types/link-api.d.ts +0 -30
  369. package/dist/types/link-cli.d.ts +0 -36
  370. package/dist/types/link-events.d.ts +0 -27
  371. package/dist/types/link-records.d.ts +0 -11
  372. package/dist/types/link-store-worker.d.ts +0 -1
  373. package/dist/types/link-store.d.ts +0 -130
  374. package/dist/types/management-policy.d.ts +0 -9
  375. package/dist/types/sqlite-version.d.ts +0 -1
  376. package/docs/DYNAMIC-LINKS.md +0 -561
  377. package/docs/MANAGEMENT-SECURITY.md +0 -82
  378. package/examples/live-links/README.md +0 -11
  379. package/examples/live-links/tests/requests.json +0 -6
  380. package/examples/live-links/urlcode.yaml +0 -16
@@ -0,0 +1,17 @@
1
+ // Field checks the runtime does not do: the body limit and JSON syntax are
2
+ // enforced before this runs. The declared signal fires after a response with
3
+ // a fixed payload (route, method, status); the message itself never leaves
4
+ // the sandbox, so a form store or mailer must sit behind the granted hook.
5
+ const emailPattern = /^[^\s@]{1,64}@[^\s@]{1,255}$/;
6
+ export default async function contact(request) {
7
+ const body = await request.json();
8
+ const errors = [];
9
+ if (body === null || typeof body !== 'object' || Array.isArray(body)) {
10
+ return Response.json({accepted: false, errors: ['body must be a JSON object']}, {status: 422});
11
+ }
12
+ if (typeof body.name !== 'string' || body.name.trim().length === 0 || body.name.length > 100) errors.push('name: 1-100 characters');
13
+ if (typeof body.email !== 'string' || !emailPattern.test(body.email)) errors.push('email: a valid address');
14
+ if (typeof body.message !== 'string' || body.message.trim().length < 10 || body.message.length > 4000) errors.push('message: 10-4000 characters');
15
+ if (errors.length) return Response.json({accepted: false, errors}, {status: 422});
16
+ return Response.json({accepted: true}, {status: 202});
17
+ }
@@ -0,0 +1,33 @@
1
+ id: contact-form
2
+ description: Validate a JSON contact message in the sandbox and notify an operator-granted hook with a fixed signal after the response.
3
+ tags: [contact, form, message, email, post, json, validation, signal, notify, hook, egress, "422"]
4
+ complexity: intermediate
5
+ capabilities: [enabled, function, methods, request.body, response.headers, signals]
6
+ targets: {self-hosted: compatible, aws: refused, vercel: refused, cloudflare: refused, static: refused}
7
+ routes: 1
8
+ services:
9
+ - name: hook endpoint
10
+ description: An HTTPS endpoint you own that receives the fixed signal payload (route, method, status); replace https://hooks.example.com/contact.
11
+ grants:
12
+ - kind: signals
13
+ description: A revision-pinned operator policy granting the hook origin, generated with `urlcode permissions` and passed as --policy outside the project.
14
+ inputs:
15
+ - name: signal url
16
+ file: urlcode.yaml
17
+ description: The hook that is notified after each response.
18
+ - name: field rules
19
+ file: functions/contact.mjs
20
+ description: The name, email and message limits.
21
+ files: [urlcode.yaml, functions/contact.mjs, tests/requests.json, README.md]
22
+ tests:
23
+ fixtures: tests/requests.json
24
+ commands:
25
+ - urlcode permissions --project . > /operator/contact-policy.json
26
+ - urlcode validate --local --project . --policy /operator/contact-policy.json
27
+ - urlcode test --project . --policy /operator/contact-policy.json
28
+ - urlcode audit --project . --expect-routes 1 --policy /operator/contact-policy.json
29
+ behavior:
30
+ - POST /contact with a valid name, email and message answers 202 {"accepted":true}
31
+ - invalid fields answer 422 with one error line per field; a non-object body answers 422
32
+ - form-encoded bodies answer 415 and GET answers 405
33
+ - the signal carries route, method and status only, best effort; the message itself never leaves the sandbox
@@ -0,0 +1,47 @@
1
+ [
2
+ {
3
+ "path": "/contact",
4
+ "method": "POST",
5
+ "headers": {
6
+ "content-type": "application/json"
7
+ },
8
+ "body": "{\"name\":\"Ada\",\"email\":\"ada@example.com\",\"message\":\"Please send the brochure.\"}",
9
+ "status": 202,
10
+ "expectBody": "{\"accepted\":true}",
11
+ "expectHeaders": {
12
+ "cache-control": "no-store"
13
+ }
14
+ },
15
+ {
16
+ "path": "/contact",
17
+ "method": "POST",
18
+ "headers": {
19
+ "content-type": "application/json"
20
+ },
21
+ "body": "{\"name\":\"\",\"email\":\"not-an-address\",\"message\":\"short\"}",
22
+ "status": 422,
23
+ "expectBody": "{\"accepted\":false,\"errors\":[\"name: 1-100 characters\",\"email: a valid address\",\"message: 10-4000 characters\"]}"
24
+ },
25
+ {
26
+ "path": "/contact",
27
+ "method": "POST",
28
+ "headers": {
29
+ "content-type": "application/json"
30
+ },
31
+ "body": "\"just a string\"",
32
+ "status": 422
33
+ },
34
+ {
35
+ "path": "/contact",
36
+ "method": "POST",
37
+ "headers": {
38
+ "content-type": "application/x-www-form-urlencoded"
39
+ },
40
+ "body": "name=Ada",
41
+ "status": 415
42
+ },
43
+ {
44
+ "path": "/contact",
45
+ "status": 405
46
+ }
47
+ ]
@@ -0,0 +1,18 @@
1
+ version: "1"
2
+ routes:
3
+ /contact:
4
+ description: Validate a JSON contact message and notify an operator-granted hook after the response.
5
+ methods: [POST]
6
+ request:
7
+ body:
8
+ required: true
9
+ maxBytes: 16384
10
+ contentTypes: [application/json]
11
+ format: json
12
+ function:
13
+ source: functions/contact.mjs
14
+ signals:
15
+ - url: https://hooks.example.com/contact
16
+ response:
17
+ headers:
18
+ Cache-Control: no-store
@@ -0,0 +1,16 @@
1
+ # CORS JSON API
2
+
3
+ Run `urlcode validate --local --project .`, `urlcode test --project .` and
4
+ `urlcode audit --project . --expect-routes 1`.
5
+
6
+ `/api/items` returns JSON from a sandboxed function. `middleware/cors.mjs` runs
7
+ around it: it answers `OPTIONS` preflight itself with 204 and adds
8
+ `Access-Control-Allow-Origin` to responses for origins in its allowlist. Other
9
+ origins get `Vary: Origin` and no allow header, so browsers refuse them.
10
+
11
+ Edit `allowedOrigins` in the middleware and the function body. The route must
12
+ list `OPTIONS` in `methods`, or the runtime answers 405 before the middleware
13
+ runs. CORS is not a host policy in this contract; middleware is the supported
14
+ place for it, and it needs the sandbox, so serverless targets refuse this
15
+ project. A browser-facing API that also needs credentials must add
16
+ `Access-Control-Allow-Credentials` deliberately, never with a wildcard origin.
@@ -0,0 +1,3 @@
1
+ export default function items() {
2
+ return Response.json({items: [{id: 1, name: 'first'}, {id: 2, name: 'second'}]});
3
+ }
@@ -0,0 +1,21 @@
1
+ // Edit this allowlist. The runtime has no CORS policy; middleware is the
2
+ // supported place for it, and it runs only on routes that declare it.
3
+ const allowedOrigins = ['https://app.example.com'];
4
+ const allowMethods = 'GET, HEAD, OPTIONS';
5
+ const allowHeaders = 'Content-Type';
6
+
7
+ export default async function cors(request, context, next) {
8
+ const origin = request.headers.get('origin');
9
+ const allowed = origin !== null && allowedOrigins.includes(origin);
10
+ if (request.method === 'OPTIONS') {
11
+ // Preflight never reaches the handler. A disallowed origin gets no CORS
12
+ // headers, so the browser refuses the real request.
13
+ const headers = {Vary: 'Origin'};
14
+ if (allowed) Object.assign(headers, {'Access-Control-Allow-Origin': origin, 'Access-Control-Allow-Methods': allowMethods, 'Access-Control-Allow-Headers': allowHeaders, 'Access-Control-Max-Age': '600'});
15
+ return new Response(null, {status: 204, headers});
16
+ }
17
+ const response = await next();
18
+ response.headers.set('Vary', 'Origin');
19
+ if (allowed) response.headers.set('Access-Control-Allow-Origin', origin);
20
+ return response;
21
+ }
@@ -0,0 +1,26 @@
1
+ id: cors-api
2
+ description: JSON endpoint for one allowed browser origin; middleware answers preflight and adds CORS headers.
3
+ tags: [cors, preflight, options, browser, origin, api, json, middleware, access-control, vary]
4
+ complexity: intermediate
5
+ capabilities: [enabled, function, methods, middleware, response.headers]
6
+ targets: {self-hosted: compatible, aws: refused, vercel: refused, cloudflare: refused, static: refused}
7
+ routes: 1
8
+ inputs:
9
+ - name: allowedOrigins
10
+ file: middleware/cors.mjs
11
+ description: The browser origins allowed to read the response.
12
+ - name: handler
13
+ file: functions/items.mjs
14
+ description: Replace the literal list with the real data.
15
+ files: [urlcode.yaml, middleware/cors.mjs, functions/items.mjs, tests/requests.json, README.md]
16
+ tests:
17
+ fixtures: tests/requests.json
18
+ commands:
19
+ - urlcode validate --local --project .
20
+ - urlcode test --project .
21
+ - urlcode audit --project . --expect-routes 1
22
+ behavior:
23
+ - OPTIONS /api/items from https://app.example.com answers 204 with Access-Control-Allow-Origin, -Methods and -Headers
24
+ - GET from the allowed origin carries Access-Control-Allow-Origin; other origins get Vary Origin and no allow header
25
+ - the route lists OPTIONS in methods, otherwise the runtime answers 405 before the middleware runs
26
+ - CORS is middleware, not a host policy, so the route needs the sandbox
@@ -0,0 +1,65 @@
1
+ [
2
+ {
3
+ "path": "/api/items",
4
+ "headers": {
5
+ "origin": "https://app.example.com"
6
+ },
7
+ "status": 200,
8
+ "expectBody": "{\"items\":[{\"id\":1,\"name\":\"first\"},{\"id\":2,\"name\":\"second\"}]}",
9
+ "expectHeaders": {
10
+ "access-control-allow-origin": "https://app.example.com",
11
+ "vary": "Origin",
12
+ "cache-control": "no-store"
13
+ }
14
+ },
15
+ {
16
+ "path": "/api/items",
17
+ "status": 200,
18
+ "expectBody": "{\"items\":[{\"id\":1,\"name\":\"first\"},{\"id\":2,\"name\":\"second\"}]}",
19
+ "expectHeaders": {
20
+ "vary": "Origin"
21
+ }
22
+ },
23
+ {
24
+ "path": "/api/items",
25
+ "method": "HEAD",
26
+ "headers": {
27
+ "origin": "https://app.example.com"
28
+ },
29
+ "status": 200,
30
+ "expectBody": "",
31
+ "expectHeaders": {
32
+ "access-control-allow-origin": "https://app.example.com"
33
+ }
34
+ },
35
+ {
36
+ "path": "/api/items",
37
+ "method": "OPTIONS",
38
+ "headers": {
39
+ "origin": "https://app.example.com",
40
+ "access-control-request-method": "GET"
41
+ },
42
+ "status": 204,
43
+ "expectBody": "",
44
+ "expectHeaders": {
45
+ "access-control-allow-origin": "https://app.example.com",
46
+ "access-control-allow-methods": "GET, HEAD, OPTIONS",
47
+ "access-control-allow-headers": "Content-Type"
48
+ }
49
+ },
50
+ {
51
+ "path": "/api/items",
52
+ "method": "OPTIONS",
53
+ "headers": {
54
+ "origin": "https://evil.example.net",
55
+ "access-control-request-method": "GET"
56
+ },
57
+ "status": 204,
58
+ "expectBody": ""
59
+ },
60
+ {
61
+ "path": "/api/items",
62
+ "method": "POST",
63
+ "status": 405
64
+ }
65
+ ]
@@ -0,0 +1,12 @@
1
+ version: "1"
2
+ routes:
3
+ /api/items:
4
+ description: JSON list callable from one allowed browser origin; middleware answers preflight and adds CORS headers.
5
+ methods: [GET, HEAD, OPTIONS]
6
+ middleware:
7
+ - source: middleware/cors.mjs
8
+ function:
9
+ source: functions/items.mjs
10
+ response:
11
+ headers:
12
+ Cache-Control: no-store
@@ -0,0 +1,13 @@
1
+ # Health and status
2
+
3
+ Run `urlcode validate --local --project .`, `urlcode test --project .` and
4
+ `urlcode audit --project . --expect-routes 2`.
5
+
6
+ `/health` answers `ok` as text and `/status` answers a small JSON document. Both
7
+ are native `respond` routes with `Cache-Control: no-store`, so no sandbox runs
8
+ and every target supports them. Change `service` in `urlcode.yaml` to your
9
+ service name.
10
+
11
+ These routes report that the runtime is serving this revision; they do not
12
+ check databases or upstreams, because native routes run no code. Operators who
13
+ want process-level detail use the runtime's own metrics endpoint instead.
@@ -0,0 +1,23 @@
1
+ id: health-page
2
+ description: Plain-text liveness and JSON readiness answers served natively with no caching.
3
+ tags: [health, status, readiness, liveness, uptime, probe, monitoring, respond, native, json]
4
+ complexity: starter
5
+ capabilities: [enabled, methods, respond, response.headers]
6
+ targets: {self-hosted: compatible, aws: compatible, vercel: compatible, cloudflare: compatible, static: refused}
7
+ routes: 2
8
+ inputs:
9
+ - name: service
10
+ file: urlcode.yaml
11
+ description: The service name in the /status document.
12
+ files: [urlcode.yaml, tests/requests.json, README.md]
13
+ tests:
14
+ fixtures: tests/requests.json
15
+ commands:
16
+ - urlcode validate --local --project .
17
+ - urlcode test --project .
18
+ - urlcode audit --project . --expect-routes 2
19
+ behavior:
20
+ - GET /health answers 200 "ok" with Cache-Control no-store
21
+ - GET /status answers 200 {"ok":true,"service":"example","schema":1}
22
+ - HEAD answers empty bodies; POST answers 405
23
+ - the routes report that this revision is serving; native routes check no upstream
@@ -0,0 +1,36 @@
1
+ [
2
+ {
3
+ "path": "/health",
4
+ "status": 200,
5
+ "expectBody": "ok",
6
+ "expectHeaders": {
7
+ "cache-control": "no-store"
8
+ }
9
+ },
10
+ {
11
+ "path": "/health",
12
+ "method": "HEAD",
13
+ "status": 200,
14
+ "expectBody": ""
15
+ },
16
+ {
17
+ "path": "/status",
18
+ "status": 200,
19
+ "expectBody": "{\"ok\":true,\"service\":\"example\",\"schema\":1}",
20
+ "expectHeaders": {
21
+ "content-type": "application/json; charset=utf-8",
22
+ "cache-control": "no-store"
23
+ }
24
+ },
25
+ {
26
+ "path": "/status",
27
+ "method": "HEAD",
28
+ "status": 200,
29
+ "expectBody": ""
30
+ },
31
+ {
32
+ "path": "/status",
33
+ "method": "POST",
34
+ "status": 405
35
+ }
36
+ ]
@@ -0,0 +1,19 @@
1
+ version: "1"
2
+ routes:
3
+ /health:
4
+ description: Plain-text liveness answer for load balancers and uptime probes.
5
+ respond:
6
+ text: ok
7
+ response:
8
+ headers:
9
+ Cache-Control: no-store
10
+ /status:
11
+ description: JSON status document with the service name and a fixed schema version.
12
+ respond:
13
+ json:
14
+ ok: true
15
+ service: example
16
+ schema: 1
17
+ response:
18
+ headers:
19
+ Cache-Control: no-store
@@ -0,0 +1,6 @@
1
+ # JSON echo API
2
+
3
+ Run `urlcode validate --local --project .` and `urlcode serve --project .`.
4
+ POST JSON with `Content-Type: application/json` to `/echo`. The sandbox returns
5
+ `{"received": ...}`. Other methods are refused. Do not submit credentials to an
6
+ echo endpoint. This recipe grants no network or filesystem access.
@@ -0,0 +1,3 @@
1
+ export default async function echo(request) {
2
+ return Response.json({received: await request.json()});
3
+ }
@@ -0,0 +1,25 @@
1
+ id: json-api
2
+ description: Bounded JSON request body echoed back by a sandboxed function.
3
+ tags: [json, api, post, echo, function, body, validation, sandbox]
4
+ complexity: starter
5
+ capabilities: [enabled, function, methods, request.body]
6
+ targets: {self-hosted: compatible, aws: refused, vercel: refused, cloudflare: refused, static: refused}
7
+ routes: 1
8
+ inputs:
9
+ - name: handler
10
+ file: functions/echo.mjs
11
+ description: Replace the echo with the real response; the parsed body arrives through request.json().
12
+ - name: maxBytes
13
+ file: urlcode.yaml
14
+ description: The request body limit (16 KiB by default).
15
+ files: [urlcode.yaml, functions/echo.mjs, tests/requests.json, README.md]
16
+ tests:
17
+ fixtures: tests/requests.json
18
+ commands:
19
+ - urlcode validate --local --project .
20
+ - urlcode test --project .
21
+ - urlcode audit --project . --expect-routes 1
22
+ behavior:
23
+ - POST /echo with application/json answers 200 {"received":<body>}
24
+ - malformed JSON answers 400 and another content type 415 before the sandbox runs
25
+ - GET answers 405
@@ -0,0 +1,34 @@
1
+ [
2
+ {
3
+ "path": "/echo",
4
+ "method": "POST",
5
+ "headers": {
6
+ "content-type": "application/json"
7
+ },
8
+ "body": "{\"hello\":\"world\"}",
9
+ "status": 200,
10
+ "expectBody": "{\"received\":{\"hello\":\"world\"}}"
11
+ },
12
+ {
13
+ "path": "/echo",
14
+ "method": "POST",
15
+ "headers": {
16
+ "content-type": "application/json"
17
+ },
18
+ "body": "{",
19
+ "status": 400
20
+ },
21
+ {
22
+ "path": "/echo",
23
+ "method": "POST",
24
+ "headers": {
25
+ "content-type": "text/plain"
26
+ },
27
+ "body": "hello",
28
+ "status": 415
29
+ },
30
+ {
31
+ "path": "/echo",
32
+ "status": 405
33
+ }
34
+ ]
@@ -0,0 +1,12 @@
1
+ version: "1"
2
+ routes:
3
+ /echo:
4
+ methods: [POST]
5
+ request:
6
+ body:
7
+ required: true
8
+ maxBytes: 16384
9
+ contentTypes: [application/json]
10
+ format: json
11
+ function:
12
+ source: functions/echo.mjs
@@ -0,0 +1,34 @@
1
+ # Middleware patterns
2
+
3
+ Fourteen reusable middleware modules on fifteen routes (`auth.mjs` serves two),
4
+ covering what Express, Koa,
5
+ Hono, Next.js and edge runtimes usually ship as middleware, rewritten for
6
+ URLCode's small sandboxed guest API. Run `urlcode validate --local --project .`
7
+ and `urlcode serve --project .`, then keep the routes you need and delete the
8
+ rest; every module is independent.
9
+
10
+ | Route | Module | Pattern |
11
+ | --- | --- | --- |
12
+ | `/api/private` | `middleware/auth.mjs` (`bearer`) | Bearer token with constant-time comparison |
13
+ | `/admin/panel` | `middleware/auth.mjs` (`basic`) | HTTP Basic decoded in the guest |
14
+ | `/cors/data` | `middleware/cors.mjs` | Origin allowlist and preflight |
15
+ | `/traced` | `middleware/request-id.mjs` | Caller correlation id and Server-Timing |
16
+ | `/maintenance` | `middleware/maintenance.mjs` | 503 kill switch with bypass header |
17
+ | `/fragile` | `middleware/errors.mjs` | Error boundary returning JSON 500 |
18
+ | `/api/items` | `middleware/envelope.mjs` | JSON response envelope |
19
+ | `/negotiated` | `middleware/negotiate.mjs` | Accept negotiation, 406 otherwise |
20
+ | `/resource` | `middleware/methods.mjs` | POST method override |
21
+ | `/versioned` | `middleware/etag.mjs` | Weak ETag and 304 for function output |
22
+ | `/experiment` | `middleware/bucket.mjs` | Sticky A/B cookie bucket |
23
+ | `/welcome` | `middleware/locale.mjs` | Accept-Language redirect |
24
+ | `/downloads/report` | `middleware/referer.mjs` | Referer allowlist on a download |
25
+ | `/profile` | `middleware/body.mjs` | Body validation handed through `context.state` |
26
+ | `/inspect` | `middleware/debug.mjs` | Redacted request echo for authoring |
27
+
28
+ Tokens, passwords and switches are literal `env` values so the recipe runs
29
+ without grants. Before deploying, move credentials to `{secret: name}` bindings
30
+ with an operator grant, replace the example.com destinations, and delete the
31
+ `/inspect` route or set `DEBUG` to `false`. The guest exposes no crypto, so none
32
+ of these modules can verify signatures or hash passwords; treat them as request
33
+ shaping, not as a security boundary. See the runtime's middleware documentation
34
+ for the full list of guest limits.
@@ -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,29 @@
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
+ // `Response.redirect()`'s headers are immutable (per the Fetch standard a
4
+ // trusted route's real `Response` enforces this, unlike the sandbox's guest
5
+ // API), so build that branch's headers up front instead of mutating the
6
+ // result afterward.
7
+ function cookie(request, name) {
8
+ for (const part of (request.headers.get('cookie') || '').split(';')) {
9
+ const [key, ...rest] = part.trim().split('=');
10
+ if (key === name) return rest.join('=');
11
+ }
12
+ return null;
13
+ }
14
+ export default async function bucket(request, context, next) {
15
+ let assigned = cookie(request, 'bucket');
16
+ const fresh = assigned !== 'a' && assigned !== 'b';
17
+ if (fresh) assigned = Math.random() < 0.5 ? 'a' : 'b';
18
+ context.state.bucket = assigned;
19
+ const setCookie = 'bucket=' + assigned + '; Path=/; Max-Age=604800; SameSite=Lax';
20
+ if (assigned === 'b') {
21
+ const headers = new Headers({ location: context.env.VARIANT_URL, vary: 'cookie' });
22
+ if (fresh) headers.append('set-cookie', setCookie);
23
+ return new Response(null, { status: 302, headers });
24
+ }
25
+ const response = await next();
26
+ response.headers.set('vary', 'cookie');
27
+ if (fresh) response.headers.append('set-cookie', setCookie);
28
+ return response;
29
+ }
@@ -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
+ }