From 6405a3c7fa9fcbf24a74fa22881006960d6d4d5a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 00:50:13 +0000 Subject: [PATCH] fix: start the Worker during isolate startup Fastify compiles routes with new Function(), which workerd only allows while the isolate is starting. v3.0.0 moved listen() into the first fetch so it could read MONGODB_URI, and every route then crashed with EvalError (Cloudflare 1101). Read the URI from cloudflare:workers at module load, and construct the Mongo client on the first bin operation because @keyv/mongo connects from its constructor. Bin capture URLs followed the plain Node socket, so an https edge request produced an http:// URL. Stamp the Worker request scheme and use it when building the public bin URL. Co-authored-by: Jared Wray --- package.json | 3 +- pnpm-lock.yaml | 965 +++++++++++++++++++++++++--- pnpm-workspace.yaml | 1 + src/routes/bins/management.ts | 39 +- test/routes/bins/management.test.ts | 50 +- test/worker-runtime.test.ts | 130 ++++ test/worker.test.ts | 86 ++- worker/app.ts | 56 +- worker/cloudflare-workers.d.ts | 3 + worker/index.ts | 28 +- wrangler.test.jsonc | 6 + 11 files changed, 1257 insertions(+), 110 deletions(-) create mode 100644 test/worker-runtime.test.ts create mode 100644 worker/cloudflare-workers.d.ts create mode 100644 wrangler.test.jsonc diff --git a/package.json b/package.json index 38a1e5b..89b2633 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,8 @@ "tsx": "^4.23.15", "typescript": "7.0.2", "vite": "^8.3.4", - "vitest": "^5.0.3" + "vitest": "^5.0.3", + "wrangler": "4.127.0" }, "dependencies": { "@fastify/cookie": "^11.1.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5096101..546fc08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -180,7 +180,7 @@ importers: version: 10.1.6 '@fastify/swagger': specifier: ^9.9.2 - version: 9.9.2(supports-color@7.2.0) + version: 9.9.2(supports-color@10.2.2) '@keyv/mongo': specifier: ^6.1.0 version: 6.1.0(keyv@6.1.0) @@ -220,7 +220,7 @@ importers: version: 5.0.3(vitest@5.0.3) docula: specifier: ^3.0.1 - version: 3.0.1(supports-color@7.2.0)(zod@4.5.4) + version: 3.0.1(supports-color@10.2.2)(zod@4.5.4) rimraf: specifier: ^6.1.3 version: 6.1.3 @@ -239,6 +239,9 @@ importers: vitest: specifier: ^5.0.3 version: 5.0.3(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(@vitest/coverage-v8@5.0.3)(vite@8.3.4(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.0)) + wrangler: + specifier: 4.127.0 + version: 4.127.0 packages: @@ -379,162 +382,368 @@ packages: '@cacheable/utils@2.5.0': resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} + '@cloudflare/kv-asset-handler@0.5.0': + resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} + engines: {node: '>=22.0.0'} + + '@cloudflare/unenv-preset@2.16.1': + resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: '>1.20260305.0 <2.0.0-0' + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20260826.1': + resolution: {integrity: sha512-8UsGGY8ZUiYHOWdsxBlNsGmaHBGArVwJ3CM4nWpfBhthjjYe4M/OqrTpqKF7NNWb63qQiv1d8Z+Z6/hmUqNKIQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260826.1': + resolution: {integrity: sha512-0bLqVQYsQ3v3FdYGmzh23vi9fJeYTBx19o4LUySIsRcgBggGSlR39ml162vTXvZzUISOjVW2qfL7Y+SMrrfXmQ==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260826.1': + resolution: {integrity: sha512-DTC0yWzybX4gUH5Q1pJo3UwEQjp0Gmz0Q71I+39xT9SXBesX5QndOIQv45yHQ86Z84EzK2WwaENdlpmDSvJKmw==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260826.1': + resolution: {integrity: sha512-PFerWi+DP2Ckc6eATAS4dhotBt8IeXJjTzIgy18H+uqsswlXc7A8HsnAunHL9v/7/BjCgq46iMo2g4iUAsEpDw==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260826.1': + resolution: {integrity: sha512-X26hulrG2MSSfpRmjZbCq98LNaZnrRqbmGcgo7g1U/U0nJ5npYruPm3fAyZ2y6VDr5CmQo9BLCahxP8VB/QR6A==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + '@esbuild/aix-ppc64@0.28.0': resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.28.0': resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.28.0': resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.28.0': resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.28.0': resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.28.0': resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.28.0': resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.28.0': resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.28.0': resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.28.0': resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.28.0': resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.28.0': resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.28.0': resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.28.0': resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.28.0': resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.28.0': resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.28.0': resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.28.0': resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.28.0': resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.28.0': resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.28.0': resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.28.0': resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.28.0': resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.28.0': resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.28.0': resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.28.0': resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@fastify/accept-negotiator@2.0.1': resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==} @@ -574,6 +783,168 @@ packages: '@fastify/swagger@9.9.2': resolution: {integrity: sha512-ayxItVgf/1axBvw33lqsA5ns02YMpT1r59G/HCJraRNWWBzvLgN+oiCBPhuzTiN67TvctlCYkG0+40WLKYuusQ==} + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.2': + resolution: {integrity: sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.2': + resolution: {integrity: sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.2': + resolution: {integrity: sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.1': + resolution: {integrity: sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.1': + resolution: {integrity: sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.1': + resolution: {integrity: sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.1': + resolution: {integrity: sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.1': + resolution: {integrity: sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.1': + resolution: {integrity: sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.1': + resolution: {integrity: sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.1': + resolution: {integrity: sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.1': + resolution: {integrity: sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.1': + resolution: {integrity: sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.2': + resolution: {integrity: sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.2': + resolution: {integrity: sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.2': + resolution: {integrity: sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.2': + resolution: {integrity: sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.2': + resolution: {integrity: sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.2': + resolution: {integrity: sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.35.2': + resolution: {integrity: sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.2': + resolution: {integrity: sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.35.2': + resolution: {integrity: sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.2': + resolution: {integrity: sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.2': + resolution: {integrity: sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.2': + resolution: {integrity: sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.2': + resolution: {integrity: sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + '@jaredwray/fumanchu@4.7.3': resolution: {integrity: sha512-YVWU9zL75VSaeMZz5+IyGP8+BOlhCBN7NTZJnypp7LMLf4SJq0csK9hK4OJnLGxw+CueVRG+TM455dEwfgBObQ==} @@ -587,6 +958,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@keyv/bigmap@1.3.1': resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} engines: {node: '>= 18'} @@ -631,6 +1005,15 @@ packages: resolution: {integrity: sha512-//0sR/cow/s4ICQaYoAobOl4aU8cjU6x/V24V7XkKotb9+O+3zySIYp146vpaobYHnxa4pZX8NkV54Z5AwbDKA==} engines: {node: '>=12'} + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} @@ -737,6 +1120,13 @@ packages: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.24': + resolution: {integrity: sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1220,6 +1610,9 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + boxen@8.0.1: resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} engines: {node: '>=18'} @@ -1469,6 +1862,9 @@ packages: resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} engines: {node: '>=20.19.0'} + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -1489,6 +1885,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + escape-goat@4.0.0: resolution: {integrity: sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==} engines: {node: '>=12'} @@ -1620,10 +2021,6 @@ packages: engines: {node: '>=0.4.7'} hasBin: true - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -1868,6 +2265,10 @@ packages: resolution: {integrity: sha512-0OO64DxKOITdyQljNCcO+vc+iyaPmm3OV5eKOWRxWCp3V0QnXenpqcKfjGjycNILUJNV+erm1ose6Hsgr1fNFA==} engines: {node: '>= 22.19.0'} + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + ky@1.14.3: resolution: {integrity: sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==} engines: {node: '>=18'} @@ -2173,6 +2574,10 @@ packages: engines: {node: '>=10.0.0'} hasBin: true + miniflare@5.20260826.0-alpha: + resolution: {integrity: sha512-ZXR3Bieg+B5MK0T/zYIWaZiCGCb9Z3en4+/TleYKhIGPLx/e0cRcG/ZvvTviUapVBBK2zODB+aiypdIojU3x6Q==} + engines: {node: '>=22.0.0'} + minimatch@10.2.4: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} @@ -2291,6 +2696,12 @@ packages: path-to-regexp@3.3.0: resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2541,6 +2952,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + serve-handler@6.1.7: resolution: {integrity: sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==} @@ -2550,6 +2966,10 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.35.2: + resolution: {integrity: sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==} + engines: {node: '>=20.9.0'} + skin-tone@2.0.0: resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} engines: {node: '>=8'} @@ -2624,9 +3044,9 @@ packages: style-to-object@2.0.2: resolution: {integrity: sha512-QmbXPUylqU80HAgTOJeEOdbA40W9KtxmjMhQy+tq+b8F2aoh3aQLuy82hh2jm2qsC9hR9eI0+eQidTMydKcqRA==} - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} @@ -2746,6 +3166,10 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici@7.29.0: + resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} + engines: {node: '>=20.18.1'} + undici@7.30.0: resolution: {integrity: sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==} engines: {node: '>=20.18.1'} @@ -2754,6 +3178,9 @@ packages: resolution: {integrity: sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==} engines: {node: '>=22.19.0'} + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + unicode-emoji-modifier-base@1.0.0: resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} engines: {node: '>=4'} @@ -2920,6 +3347,21 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + workerd@1.20260826.1: + resolution: {integrity: sha512-oTG9ot5zxO9OjjKCskt86+nVrBL0kiUqExHnYaTg4OCkHf/K4zr+9hYvgwmG8DdCWG0TQGErHzD+1h50TM5b+w==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.127.0: + resolution: {integrity: sha512-4dPqcBEMJfGeZeNnjHT7ThNJs+EiNYxUTg4ywqIdQubcXHBhFeVMQyHV4A9AOhZRFr83cckqdds034KGcr/dtw==} + engines: {node: '>=22.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^5.20260826.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -2931,6 +3373,18 @@ packages: resolution: {integrity: sha512-YtzHWvNo1HMmiQ5lckRYjbl7gEovJLXWrs6aUaa4m35pnHcbmbjPqWpkFyuo9aUWjVeuiDuTELBiAebKnX7RNA==} engines: {node: ^22.18.0 || >=24.0.0} + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xdg-basedir@5.1.0: resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} engines: {node: '>=12'} @@ -2940,6 +3394,12 @@ packages: engines: {node: '>= 14.6'} hasBin: true + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + yuku-ast@0.18.1: resolution: {integrity: sha512-Y9YgmYeExF+13IOm4r9pTgmQiCVJtrhq014st78eAckWlCkbjzNd2AVrQHng5uiLz9j31O4sGYhuP3znSKJv9w==} engines: {node: '>=14.18.0'} @@ -3092,84 +3552,194 @@ snapshots: hashery: 1.5.1 keyv: 5.6.0 + '@cloudflare/kv-asset-handler@0.5.0': {} + + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260826.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260826.1 + + '@cloudflare/workerd-darwin-64@1.20260826.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260826.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20260826.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260826.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20260826.1': + optional: true + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.28.0': optional: true + '@esbuild/aix-ppc64@0.28.1': + optional: true + '@esbuild/android-arm64@0.28.0': optional: true + '@esbuild/android-arm64@0.28.1': + optional: true + '@esbuild/android-arm@0.28.0': optional: true + '@esbuild/android-arm@0.28.1': + optional: true + '@esbuild/android-x64@0.28.0': optional: true + '@esbuild/android-x64@0.28.1': + optional: true + '@esbuild/darwin-arm64@0.28.0': optional: true + '@esbuild/darwin-arm64@0.28.1': + optional: true + '@esbuild/darwin-x64@0.28.0': optional: true + '@esbuild/darwin-x64@0.28.1': + optional: true + '@esbuild/freebsd-arm64@0.28.0': optional: true + '@esbuild/freebsd-arm64@0.28.1': + optional: true + '@esbuild/freebsd-x64@0.28.0': optional: true + '@esbuild/freebsd-x64@0.28.1': + optional: true + '@esbuild/linux-arm64@0.28.0': optional: true + '@esbuild/linux-arm64@0.28.1': + optional: true + '@esbuild/linux-arm@0.28.0': optional: true + '@esbuild/linux-arm@0.28.1': + optional: true + '@esbuild/linux-ia32@0.28.0': optional: true + '@esbuild/linux-ia32@0.28.1': + optional: true + '@esbuild/linux-loong64@0.28.0': optional: true + '@esbuild/linux-loong64@0.28.1': + optional: true + '@esbuild/linux-mips64el@0.28.0': optional: true + '@esbuild/linux-mips64el@0.28.1': + optional: true + '@esbuild/linux-ppc64@0.28.0': optional: true + '@esbuild/linux-ppc64@0.28.1': + optional: true + '@esbuild/linux-riscv64@0.28.0': optional: true + '@esbuild/linux-riscv64@0.28.1': + optional: true + '@esbuild/linux-s390x@0.28.0': optional: true + '@esbuild/linux-s390x@0.28.1': + optional: true + '@esbuild/linux-x64@0.28.0': optional: true + '@esbuild/linux-x64@0.28.1': + optional: true + '@esbuild/netbsd-arm64@0.28.0': optional: true + '@esbuild/netbsd-arm64@0.28.1': + optional: true + '@esbuild/netbsd-x64@0.28.0': optional: true + '@esbuild/netbsd-x64@0.28.1': + optional: true + '@esbuild/openbsd-arm64@0.28.0': optional: true + '@esbuild/openbsd-arm64@0.28.1': + optional: true + '@esbuild/openbsd-x64@0.28.0': optional: true + '@esbuild/openbsd-x64@0.28.1': + optional: true + '@esbuild/openharmony-arm64@0.28.0': optional: true + '@esbuild/openharmony-arm64@0.28.1': + optional: true + '@esbuild/sunos-x64@0.28.0': optional: true + '@esbuild/sunos-x64@0.28.1': + optional: true + '@esbuild/win32-arm64@0.28.0': optional: true + '@esbuild/win32-arm64@0.28.1': + optional: true + '@esbuild/win32-ia32@0.28.0': optional: true + '@esbuild/win32-ia32@0.28.1': + optional: true + '@esbuild/win32-x64@0.28.0': optional: true + '@esbuild/win32-x64@0.28.1': + optional: true + '@fastify/accept-negotiator@2.0.1': {} '@fastify/ajv-compiler@4.0.7': @@ -3230,16 +3800,122 @@ snapshots: fastq: 1.20.1 glob: 13.0.6 - '@fastify/swagger@9.9.2(supports-color@7.2.0)': + '@fastify/swagger@9.9.2(supports-color@10.2.2)': dependencies: fastify-plugin: 6.0.0 - json-schema-resolver: 3.0.0(supports-color@7.2.0) + json-schema-resolver: 3.0.0(supports-color@10.2.2) openapi-types: 12.1.3 rfdc: 1.4.1 yaml: 2.9.0 transitivePeerDependencies: - supports-color + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.1 + optional: true + + '@img/sharp-darwin-x64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.1 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.2': + dependencies: + '@img/sharp-wasm32': 0.35.2 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.1': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.1': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.1': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.1': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.1': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.1': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.1': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.1': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.1': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.1': + optional: true + + '@img/sharp-linux-arm64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.1 + optional: true + + '@img/sharp-linux-arm@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.1 + optional: true + + '@img/sharp-linux-ppc64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.1 + optional: true + + '@img/sharp-linux-riscv64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.1 + optional: true + + '@img/sharp-linux-s390x@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.1 + optional: true + + '@img/sharp-linux-x64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.1 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.1 + optional: true + + '@img/sharp-wasm32@0.35.2': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.2': + dependencies: + '@img/sharp-wasm32': 0.35.2 + optional: true + + '@img/sharp-win32-arm64@0.35.2': + optional: true + + '@img/sharp-win32-ia32@0.35.2': + optional: true + + '@img/sharp-win32-x64@0.35.2': + optional: true + '@jaredwray/fumanchu@4.7.3': dependencies: '@cacheable/memory': 2.2.0 @@ -3258,6 +3934,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + '@keyv/bigmap@1.3.1(keyv@5.6.0)': dependencies: hashery: 1.5.1 @@ -3305,6 +3986,18 @@ snapshots: '@pnpm/network.ca-file': 1.0.2 config-chain: 1.1.13 + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + '@quansync/fs@1.0.0': dependencies: quansync: 1.0.0 @@ -3358,6 +4051,10 @@ snapshots: '@sindresorhus/is@4.6.0': {} + '@sindresorhus/is@7.2.0': {} + + '@speed-highlight/core@1.2.24': {} + '@standard-schema/spec@1.1.0': {} '@swc/core-darwin-arm64@1.16.18': @@ -3694,6 +4391,8 @@ snapshots: balanced-match@4.0.4: {} + blake3-wasm@2.1.5: {} + boxen@8.0.1: dependencies: ansi-align: 3.0.1 @@ -3809,11 +4508,11 @@ snapshots: dayjs@1.11.23: {} - debug@4.4.3(supports-color@7.2.0): + debug@4.4.3(supports-color@10.2.2): dependencies: ms: 2.1.3 optionalDependencies: - supports-color: 7.2.0 + supports-color: 10.2.2 decode-named-character-reference@1.3.0: dependencies: @@ -3839,7 +4538,7 @@ snapshots: doctypes@1.1.0: {} - docula@3.0.1(supports-color@7.2.0)(zod@4.5.4): + docula@3.0.1(supports-color@10.2.2)(zod@4.5.4): dependencies: '@ai-sdk/anthropic': 4.0.78(zod@4.5.4) '@ai-sdk/google': 4.0.93(zod@4.5.4) @@ -3847,14 +4546,14 @@ snapshots: '@cacheable/net': 2.1.0 ai: 7.0.137(zod@4.5.4) colorette: 2.0.20 - ecto: 5.1.0(supports-color@7.2.0) + ecto: 5.1.0(supports-color@10.2.2) hashery: 3.0.1 ipaddr.js: 2.5.0 jiti: 2.7.0 serve-handler: 6.1.7 undici: 8.11.2 update-notifier: 7.3.1 - writr: 6.1.5(supports-color@7.2.0) + writr: 6.1.5(supports-color@10.2.2) transitivePeerDependencies: - '@types/react' - chokidar @@ -3891,7 +4590,7 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - ecto@5.1.0(supports-color@7.2.0): + ecto@5.1.0(supports-color@10.2.2): dependencies: '@jaredwray/fumanchu': 4.7.3 cacheable: 2.5.0 @@ -3900,7 +4599,7 @@ snapshots: liquidjs: 10.29.0 nunjucks: 3.2.4 pug: 3.0.4 - writr: 6.1.5(supports-color@7.2.0) + writr: 6.1.5(supports-color@10.2.2) transitivePeerDependencies: - '@types/react' - chokidar @@ -3926,6 +4625,8 @@ snapshots: entities@8.0.0: {} + error-stack-parser-es@1.0.5: {} + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -3965,6 +4666,35 @@ snapshots: '@esbuild/win32-ia32': 0.28.0 '@esbuild/win32-x64': 0.28.0 + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + escape-goat@4.0.0: {} escape-html@1.0.3: {} @@ -4107,9 +4837,6 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 - has-flag@4.0.0: - optional: true - has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -4362,9 +5089,9 @@ snapshots: dependencies: dequal: 2.0.3 - json-schema-resolver@3.0.0(supports-color@7.2.0): + json-schema-resolver@3.0.0(supports-color@10.2.2): dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@10.2.2) fast-uri: 3.1.8 rfdc: 1.4.1 transitivePeerDependencies: @@ -4391,6 +5118,8 @@ snapshots: dependencies: hookified: 3.0.4 + kleur@4.1.5: {} + ky@1.14.3: {} latest-version@9.0.0: @@ -4500,14 +5229,14 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 - mdast-util-from-markdown@2.0.3(supports-color@7.2.0): + mdast-util-from-markdown@2.0.3(supports-color@10.2.2): dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 decode-named-character-reference: 1.3.0 devlop: 1.1.0 mdast-util-to-string: 4.0.0 - micromark: 4.0.2(supports-color@7.2.0) + micromark: 4.0.2(supports-color@10.2.2) micromark-util-decode-numeric-character-reference: 2.0.2 micromark-util-decode-string: 2.0.1 micromark-util-normalize-identifier: 2.0.1 @@ -4525,79 +5254,79 @@ snapshots: mdast-util-find-and-replace: 3.0.2 micromark-util-character: 2.1.1 - mdast-util-gfm-footnote@2.1.0(supports-color@7.2.0): + mdast-util-gfm-footnote@2.1.0(supports-color@10.2.2): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) + mdast-util-from-markdown: 2.0.3(supports-color@10.2.2) mdast-util-to-markdown: 2.1.2 micromark-util-normalize-identifier: 2.0.1 transitivePeerDependencies: - supports-color - mdast-util-gfm-strikethrough@2.0.0(supports-color@7.2.0): + mdast-util-gfm-strikethrough@2.0.0(supports-color@10.2.2): dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) + mdast-util-from-markdown: 2.0.3(supports-color@10.2.2) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm-table@2.0.0(supports-color@7.2.0): + mdast-util-gfm-table@2.0.0(supports-color@10.2.2): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 markdown-table: 3.0.4 - mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) + mdast-util-from-markdown: 2.0.3(supports-color@10.2.2) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm-task-list-item@2.0.0(supports-color@7.2.0): + mdast-util-gfm-task-list-item@2.0.0(supports-color@10.2.2): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) + mdast-util-from-markdown: 2.0.3(supports-color@10.2.2) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm@3.1.0(supports-color@7.2.0): + mdast-util-gfm@3.1.0(supports-color@10.2.2): dependencies: - mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) + mdast-util-from-markdown: 2.0.3(supports-color@10.2.2) mdast-util-gfm-autolink-literal: 2.0.1 - mdast-util-gfm-footnote: 2.1.0(supports-color@7.2.0) - mdast-util-gfm-strikethrough: 2.0.0(supports-color@7.2.0) - mdast-util-gfm-table: 2.0.0(supports-color@7.2.0) - mdast-util-gfm-task-list-item: 2.0.0(supports-color@7.2.0) + mdast-util-gfm-footnote: 2.1.0(supports-color@10.2.2) + mdast-util-gfm-strikethrough: 2.0.0(supports-color@10.2.2) + mdast-util-gfm-table: 2.0.0(supports-color@10.2.2) + mdast-util-gfm-task-list-item: 2.0.0(supports-color@10.2.2) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-math@3.0.0(supports-color@7.2.0): + mdast-util-math@3.0.0(supports-color@10.2.2): dependencies: '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 longest-streak: 3.1.0 - mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) + mdast-util-from-markdown: 2.0.3(supports-color@10.2.2) mdast-util-to-markdown: 2.1.2 unist-util-remove-position: 5.0.0 transitivePeerDependencies: - supports-color - mdast-util-mdx-expression@2.0.1(supports-color@7.2.0): + mdast-util-mdx-expression@2.0.1(supports-color@10.2.2): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) + mdast-util-from-markdown: 2.0.3(supports-color@10.2.2) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-mdx-jsx@3.2.0(supports-color@7.2.0): + mdast-util-mdx-jsx@3.2.0(supports-color@10.2.2): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 @@ -4605,7 +5334,7 @@ snapshots: '@types/unist': 3.0.3 ccount: 2.0.1 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) + mdast-util-from-markdown: 2.0.3(supports-color@10.2.2) mdast-util-to-markdown: 2.1.2 parse-entities: 4.0.2 stringify-entities: 4.0.4 @@ -4614,23 +5343,23 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-mdx@3.0.0(supports-color@7.2.0): + mdast-util-mdx@3.0.0(supports-color@10.2.2): dependencies: - mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) - mdast-util-mdx-expression: 2.0.1(supports-color@7.2.0) - mdast-util-mdx-jsx: 3.2.0(supports-color@7.2.0) - mdast-util-mdxjs-esm: 2.0.1(supports-color@7.2.0) + mdast-util-from-markdown: 2.0.3(supports-color@10.2.2) + mdast-util-mdx-expression: 2.0.1(supports-color@10.2.2) + mdast-util-mdx-jsx: 3.2.0(supports-color@10.2.2) + mdast-util-mdxjs-esm: 2.0.1(supports-color@10.2.2) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-mdxjs-esm@2.0.1(supports-color@7.2.0): + mdast-util-mdxjs-esm@2.0.1(supports-color@10.2.2): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) + mdast-util-from-markdown: 2.0.3(supports-color@10.2.2) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -4934,10 +5663,10 @@ snapshots: micromark-util-types@2.0.2: {} - micromark@4.0.2(supports-color@7.2.0): + micromark@4.0.2(supports-color@10.2.2): dependencies: '@types/debug': 4.1.13 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@10.2.2) decode-named-character-reference: 1.3.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -4969,6 +5698,18 @@ snapshots: mime@3.0.0: {} + miniflare@5.20260826.0-alpha: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.35.2 + undici: 7.29.0 + workerd: 1.20260826.1 + ws: 8.21.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + minimatch@10.2.4: dependencies: brace-expansion: 5.0.12 @@ -5059,6 +5800,10 @@ snapshots: path-to-regexp@3.3.0: {} + path-to-regexp@6.3.0: {} + + pathe@2.0.3: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -5278,12 +6023,12 @@ snapshots: node-emoji: 2.2.0 unified: 11.0.5 - remark-gfm@4.0.1(supports-color@7.2.0): + remark-gfm@4.0.1(supports-color@10.2.2): dependencies: '@types/mdast': 4.0.4 - mdast-util-gfm: 3.1.0(supports-color@7.2.0) + mdast-util-gfm: 3.1.0(supports-color@10.2.2) micromark-extension-gfm: 3.0.0 - remark-parse: 11.0.0(supports-color@7.2.0) + remark-parse: 11.0.0(supports-color@10.2.2) remark-stringify: 11.0.0 unified: 11.0.5 transitivePeerDependencies: @@ -5293,26 +6038,26 @@ snapshots: dependencies: unist-util-visit: 5.1.0 - remark-math@6.0.0(supports-color@7.2.0): + remark-math@6.0.0(supports-color@10.2.2): dependencies: '@types/mdast': 4.0.4 - mdast-util-math: 3.0.0(supports-color@7.2.0) + mdast-util-math: 3.0.0(supports-color@10.2.2) micromark-extension-math: 3.1.0 unified: 11.0.5 transitivePeerDependencies: - supports-color - remark-mdx@3.1.1(supports-color@7.2.0): + remark-mdx@3.1.1(supports-color@10.2.2): dependencies: - mdast-util-mdx: 3.0.0(supports-color@7.2.0) + mdast-util-mdx: 3.0.0(supports-color@10.2.2) micromark-extension-mdxjs: 3.0.0 transitivePeerDependencies: - supports-color - remark-parse@11.0.0(supports-color@7.2.0): + remark-parse@11.0.0(supports-color@10.2.2): dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) + mdast-util-from-markdown: 2.0.3(supports-color@10.2.2) micromark-util-types: 2.0.2 unified: 11.0.5 transitivePeerDependencies: @@ -5404,6 +6149,8 @@ snapshots: semver@7.7.4: {} + semver@7.8.5: {} + serve-handler@6.1.7: dependencies: bytes: 3.0.0 @@ -5418,6 +6165,38 @@ snapshots: setprototypeof@1.2.0: {} + sharp@0.35.2: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.2 + '@img/sharp-darwin-x64': 0.35.2 + '@img/sharp-freebsd-wasm32': 0.35.2 + '@img/sharp-libvips-darwin-arm64': 1.3.1 + '@img/sharp-libvips-darwin-x64': 1.3.1 + '@img/sharp-libvips-linux-arm': 1.3.1 + '@img/sharp-libvips-linux-arm64': 1.3.1 + '@img/sharp-libvips-linux-ppc64': 1.3.1 + '@img/sharp-libvips-linux-riscv64': 1.3.1 + '@img/sharp-libvips-linux-s390x': 1.3.1 + '@img/sharp-libvips-linux-x64': 1.3.1 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 + '@img/sharp-libvips-linuxmusl-x64': 1.3.1 + '@img/sharp-linux-arm': 0.35.2 + '@img/sharp-linux-arm64': 0.35.2 + '@img/sharp-linux-ppc64': 0.35.2 + '@img/sharp-linux-riscv64': 0.35.2 + '@img/sharp-linux-s390x': 0.35.2 + '@img/sharp-linux-x64': 0.35.2 + '@img/sharp-linuxmusl-arm64': 0.35.2 + '@img/sharp-linuxmusl-x64': 0.35.2 + '@img/sharp-webcontainers-wasm32': 0.35.2 + '@img/sharp-win32-arm64': 0.35.2 + '@img/sharp-win32-ia32': 0.35.2 + '@img/sharp-win32-x64': 0.35.2 + skin-tone@2.0.0: dependencies: unicode-emoji-modifier-base: 1.0.0 @@ -5489,10 +6268,7 @@ snapshots: dependencies: inline-style-parser: 0.2.9 - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - optional: true + supports-color@10.2.2: {} supports-preserve-symlinks-flag@1.0.0: {} @@ -5604,10 +6380,16 @@ snapshots: undici-types@7.16.0: optional: true + undici@7.29.0: {} + undici@7.30.0: {} undici@8.11.2: {} + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + unicode-emoji-modifier-base@1.0.0: {} unified@11.0.5: @@ -5753,6 +6535,30 @@ snapshots: wordwrap@1.0.0: {} + workerd@1.20260826.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260826.1 + '@cloudflare/workerd-darwin-arm64': 1.20260826.1 + '@cloudflare/workerd-linux-64': 1.20260826.1 + '@cloudflare/workerd-linux-arm64': 1.20260826.1 + '@cloudflare/workerd-windows-64': 1.20260826.1 + + wrangler@4.127.0: + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260826.1) + blake3-wasm: 2.1.5 + esbuild: 0.28.1 + miniflare: 5.20260826.0-alpha + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260826.1 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 @@ -5761,7 +6567,7 @@ snapshots: wrappy@1.0.2: {} - writr@6.1.5(supports-color@7.2.0): + writr@6.1.5(supports-color@10.2.2): dependencies: ai: 7.0.91(zod@4.5.4) cacheable: 2.5.0 @@ -5776,11 +6582,11 @@ snapshots: rehype-slug: 6.0.0 rehype-stringify: 10.0.1 remark-emoji: 5.0.2 - remark-gfm: 4.0.1(supports-color@7.2.0) + remark-gfm: 4.0.1(supports-color@10.2.2) remark-github-blockquote-alert: 2.1.0 - remark-math: 6.0.0(supports-color@7.2.0) - remark-mdx: 3.1.1(supports-color@7.2.0) - remark-parse: 11.0.0(supports-color@7.2.0) + remark-math: 6.0.0(supports-color@10.2.2) + remark-mdx: 3.1.1(supports-color@10.2.2) + remark-parse: 11.0.0(supports-color@10.2.2) remark-rehype: 11.1.2 remark-toc: 9.0.0 unified: 11.0.5 @@ -5789,10 +6595,25 @@ snapshots: - '@types/react' - supports-color + ws@8.21.0: {} + xdg-basedir@5.1.0: {} yaml@2.9.0: {} + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.24 + cookie: 1.1.1 + youch-core: 0.3.3 + yuku-ast@0.18.1: dependencies: '@yuku-toolchain/types': 0.18.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 06860da..0809869 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,3 +9,4 @@ trustPolicy: no-downgrade allowBuilds: '@swc/core': true esbuild: true + workerd: true diff --git a/src/routes/bins/management.ts b/src/routes/bins/management.ts index 3efca55..11ec30b 100644 --- a/src/routes/bins/management.ts +++ b/src/routes/bins/management.ts @@ -188,6 +188,37 @@ type RequestIdRequest = FastifyRequest<{ Params: { id: string; reqId: string }; }>; +/** + * Set by the Worker from the incoming request URL. The Node bridge rewrites + * `x-forwarded-proto` to the plain-HTTP socket, so that header cannot carry + * the public scheme. + */ +export const PUBLIC_PROTOCOL_HEADER = "x-mockhttp-forwarded-proto"; + +/** + * Public capture URL for a new bin. + * The last forwarded protocol wins, matching a proxy chain. An unrecognized + * value falls back to the socket protocol, then `http`. + */ +export function binCaptureUrl( + forwardedProto: string | string[] | undefined, + socketProtocol: string | undefined, + host: string, + id: string, +): string { + const raw = Array.isArray(forwardedProto) + ? forwardedProto.join(",") + : forwardedProto; + const forwarded = raw?.split(",").at(-1)?.trim().toLowerCase(); + let protocol = "http"; + if (forwarded === "https" || forwarded === "http") { + protocol = forwarded; + } else if (socketProtocol === "https") { + protocol = "https"; + } + return `${protocol}://${host}/b/${id}`; +} + /** * Create a Fastify plugin that registers bin management endpoints, with the * provided {@link MockHttpBins} injected. @@ -203,7 +234,13 @@ export const binsManagementRoute = (bins: MockHttpBins) => { const host = request.headers.host ?? request.hostname; return { ...bin, - url: `${request.protocol}://${host}/b/${bin.id}`, + url: binCaptureUrl( + request.headers[PUBLIC_PROTOCOL_HEADER] ?? + request.headers["x-forwarded-proto"], + request.protocol, + host, + bin.id, + ), }; }, ); diff --git a/test/routes/bins/management.test.ts b/test/routes/bins/management.test.ts index 604a58b..01ab642 100644 --- a/test/routes/bins/management.test.ts +++ b/test/routes/bins/management.test.ts @@ -2,7 +2,10 @@ import Fastify, { type FastifyInstance } from "fastify"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { MockHttpBins } from "../../../src/mock-http-bins.js"; import { binsCaptureRoute } from "../../../src/routes/bins/capture.js"; -import { binsManagementRoute } from "../../../src/routes/bins/management.js"; +import { + binCaptureUrl, + binsManagementRoute, +} from "../../../src/routes/bins/management.js"; describe("Bin management routes", () => { let fastify: FastifyInstance; @@ -30,6 +33,51 @@ describe("Bin management routes", () => { expect(body.requestCount).toBe(0); }); + it("POST /bins uses the forwarded protocol in the capture URL", async () => { + const res = await fastify.inject({ + method: "POST", + url: "/bins", + headers: { + host: "mockhttp.org", + "x-forwarded-proto": "https", + }, + }); + expect(res.statusCode).toBe(200); + const body = res.json(); + expect(body.url).toBe(`https://mockhttp.org/b/${body.id}`); + + const workerScheme = await fastify.inject({ + method: "POST", + url: "/bins", + headers: { + host: "mockhttp.org", + "x-forwarded-proto": "http", + "x-mockhttp-forwarded-proto": "https", + }, + }); + expect(workerScheme.statusCode).toBe(200); + const workerBody = workerScheme.json(); + expect(workerBody.url).toBe(`https://mockhttp.org/b/${workerBody.id}`); + }); + + it("builds a capture URL from the forwarded protocol chain", () => { + expect(binCaptureUrl("http, https", "http", "mockhttp.org", "abc")).toBe( + "https://mockhttp.org/b/abc", + ); + expect( + binCaptureUrl(["http", "https"], "http", "mockhttp.org", "abc"), + ).toBe("https://mockhttp.org/b/abc"); + expect(binCaptureUrl("ftp", "https", "mockhttp.org", "abc")).toBe( + "https://mockhttp.org/b/abc", + ); + expect(binCaptureUrl(undefined, "http", "localhost:3000", "abc")).toBe( + "http://localhost:3000/b/abc", + ); + expect(binCaptureUrl(" ", undefined, "localhost", "abc")).toBe( + "http://localhost/b/abc", + ); + }); + it("POST /bins returns 500 when Keyv does not store the bin", async () => { vi.spyOn(manager.keyv, "set").mockResolvedValue(false); const res = await fastify.inject({ method: "POST", url: "/bins" }); diff --git a/test/worker-runtime.test.ts b/test/worker-runtime.test.ts new file mode 100644 index 0000000..feb0e81 --- /dev/null +++ b/test/worker-runtime.test.ts @@ -0,0 +1,130 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { type Unstable_DevWorker, unstable_dev } from "wrangler"; + +// wrangler's HTTPS dev listener uses a self-signed certificate. +process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + +const devOptions = { + config: "wrangler.test.jsonc", + experimental: { + disableExperimentalWarning: true, + disableDevRegistry: true, + watch: false, + }, + persist: false, + ip: "127.0.0.1", + local: true, + logLevel: "error" as const, +}; + +/** + * Boots the Worker entry inside workerd. Fastify's route compiler calls + * `new Function()`, which workerd only allows during isolate startup. A + * regression that moves `listen` into `fetch` fails these requests. + */ +describe("worker under workerd", () => { + let worker: Unstable_DevWorker; + + beforeAll(async () => { + worker = await unstable_dev("worker/index.ts", { + ...devOptions, + port: 18787, + inspectorPort: 19229, + // Production Workers see the client https URL. The default wrangler + // listener is http and would hide that. The dev proxy's https mode + // is what the stamp has to follow. + localProtocol: "https", + }); + }, 120_000); + + afterAll(async () => { + await worker?.stop(); + }); + + it("serves mock routes and https bin URLs", async () => { + const get = await worker.fetch("https://mockhttp.org/get"); + expect(get.status, await textOf(get)).toBe(200); + expect(await get.json()).toMatchObject({ method: "GET" }); + + const ip = await worker.fetch("https://mockhttp.org/ip", { + headers: { "cf-connecting-ip": "203.0.113.10" }, + }); + expect(ip.status, await textOf(ip)).toBe(200); + expect(await ip.json()).toMatchObject({ ip: "203.0.113.10" }); + + const listed = await worker.fetch("https://mockhttp.org/bins"); + expect(listed.status, await textOf(listed)).toBe(200); + expect(await listed.json()).toEqual({ bins: [] }); + + const created = await worker.fetch("https://mockhttp.org/bins", { + method: "POST", + }); + expect(created.status, await textOf(created)).toBe(200); + const bin = (await created.json()) as { id: string; url: string }; + expect(bin.url).toBe(`https://mockhttp.org/b/${bin.id}`); + + const captured = await worker.fetch(`https://mockhttp.org/b/${bin.id}`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ ok: true }), + }); + expect(captured.status, await textOf(captured)).toBe(200); + expect(await captured.json()).toMatchObject({ ok: true, binId: bin.id }); + + const localHttp = await unstable_dev("worker/index.ts", { + ...devOptions, + port: 18789, + inspectorPort: 19231, + localProtocol: "http", + }); + try { + const httpBin = await localHttp.fetch("https://mockhttp.org/bins", { + method: "POST", + }); + expect(httpBin.status, await textOf(httpBin)).toBe(200); + const httpBody = (await httpBin.json()) as { id: string; url: string }; + // wrangler dev's default listener is HTTP, so the Worker request URL + // is http even when the test asked for https. The bin URL follows that. + expect(httpBody.url).toBe(`http://mockhttp.org/b/${httpBody.id}`); + } finally { + await localHttp.stop(); + } + }, 60_000); +}); + +describe("worker mongo connect under workerd", () => { + let worker: Unstable_DevWorker; + + beforeAll(async () => { + worker = await unstable_dev("worker/index.ts", { + ...devOptions, + port: 18788, + inspectorPort: 19230, + // Nothing is listening here. Startup must still succeed; the client + // connects on the first bin write. + vars: { + MONGODB_URI: "mongodb://127.0.0.1:1/mockhttp", + }, + }); + }, 120_000); + + afterAll(async () => { + await worker?.stop(); + }); + + it("does not connect to MongoDB until a bin is used", async () => { + const get = await worker.fetch("https://mockhttp.org/get"); + expect(get.status, await textOf(get)).toBe(200); + + const created = await worker.fetch("https://mockhttp.org/bins", { + method: "POST", + }); + const body = await created.text(); + expect(created.status, body).toBe(500); + expect(body).not.toContain("Code generation from strings"); + }, 60_000); +}); + +async function textOf(response: Response): Promise { + return response.clone().text(); +} diff --git a/test/worker.test.ts b/test/worker.test.ts index ced3308..3597012 100644 --- a/test/worker.test.ts +++ b/test/worker.test.ts @@ -68,6 +68,7 @@ const { createWorkerApp, handleWorkerFetch, listenWorkerApp, + requestWithPublicProtocol, workerMockHttpOptions, workerMongoUri, workerRuntime, @@ -166,7 +167,11 @@ describe("cloudflare worker", () => { ); expect(limit).toHaveBeenCalledWith({ key: "unknown" }); - expect(dispatch).toHaveBeenCalledWith(WORKER_PORT, request); + expect(dispatch).toHaveBeenCalledOnce(); + const forwarded = dispatch.mock.calls[0]?.[1] as Request; + expect(dispatch.mock.calls[0]?.[0]).toBe(WORKER_PORT); + expect(forwarded.url).toBe(request.url); + expect(forwarded.headers.get("x-mockhttp-forwarded-proto")).toBe("https"); expect(response).toBe(expected); }); @@ -223,10 +228,46 @@ describe("cloudflare worker", () => { handleAsNodeRequestMock.mockResolvedValue(new Response("ok")); const request = new Request("https://mockhttp.org/ip"); const response = await handleWorkerFetch(request, {}); - expect(handleAsNodeRequestMock).toHaveBeenCalledWith(WORKER_PORT, request); + expect(handleAsNodeRequestMock).toHaveBeenCalledOnce(); + const forwarded = handleAsNodeRequestMock.mock.calls[0]?.[1] as Request; + expect(handleAsNodeRequestMock.mock.calls[0]?.[0]).toBe(WORKER_PORT); + expect(forwarded.url).toBe(request.url); + expect(forwarded.headers.get("x-mockhttp-forwarded-proto")).toBe("https"); expect(response.status).toBe(200); expect(await response.text()).toBe("ok"); }); + + it("stamps the worker request scheme onto x-forwarded-proto", () => { + const request = new Request("https://mockhttp.org/bins", { + method: "POST", + }); + const stamped = requestWithPublicProtocol(request); + expect(stamped).not.toBe(request); + expect(stamped.method).toBe("POST"); + expect(stamped.url).toBe(request.url); + expect(stamped.headers.get("x-mockhttp-forwarded-proto")).toBe("https"); + }); + + it("leaves a request alone when the public protocol header already matches", () => { + const request = new Request("http://127.0.0.1/bins", { + headers: { "x-mockhttp-forwarded-proto": "http" }, + }); + expect(requestWithPublicProtocol(request)).toBe(request); + }); + + it("replaces a public protocol header that disagrees with the request URL", () => { + const request = new Request("https://mockhttp.org/bins", { + headers: { + "x-forwarded-proto": "http", + "x-mockhttp-forwarded-proto": "http", + "x-request-id": "abc", + }, + }); + const stamped = requestWithPublicProtocol(request); + expect(stamped.headers.get("x-mockhttp-forwarded-proto")).toBe("https"); + expect(stamped.headers.get("x-forwarded-proto")).toBe("http"); + expect(stamped.headers.get("x-request-id")).toBe("abc"); + }); }); describe("worker Fastify app", () => { @@ -305,7 +346,22 @@ describe("worker Fastify app", () => { process.env.MONGODB_URI = "mongodb://from-process-env"; const mockHttp = await createWorkerApp({ MONGODB_URI: uri }); + // The client must not be constructed while the Worker isolate is starting. + expect(KeyvMongoMock.instances).toHaveLength(0); + const untouched = await mockHttp.server.inject({ + method: "GET", + url: "/get", + }); + expect(untouched.statusCode).toBe(200); + expect(KeyvMongoMock.instances).toHaveLength(0); + + const created = await mockHttp.server.inject({ + method: "POST", + url: "/bins", + }); + expect(created.statusCode).toBe(200); const store = KeyvMongoMock.instances[0]; + expect(KeyvMongoMock.instances).toHaveLength(1); expect(store?.url).toBe(uri); expect(store?.options).toEqual({ collection: "bins", @@ -313,12 +369,6 @@ describe("worker Fastify app", () => { minPoolSize: 0, serverSelectionTimeoutMS: 5_000, }); - - const created = await mockHttp.server.inject({ - method: "POST", - url: "/bins", - }); - expect(created.statusCode).toBe(200); const id = created.json().id as string; expect(store?.data.size).toBeGreaterThan(0); @@ -328,6 +378,22 @@ describe("worker Fastify app", () => { }); expect(fetched.statusCode).toBe(200); expect(fetched.json()).toMatchObject({ id }); + + const listed = await mockHttp.server.inject({ + method: "GET", + url: "/bins", + }); + expect(listed.statusCode).toBe(200); + expect(listed.json().bins).toEqual( + expect.arrayContaining([expect.objectContaining({ id })]), + ); + + const removed = await mockHttp.server.inject({ + method: "DELETE", + url: `/bins/${id}`, + }); + expect(removed.statusCode).toBe(204); + await mockHttp.bins.keyv.clear(); await mockHttp.close(); delete process.env.MONGODB_URI; }); @@ -339,12 +405,14 @@ describe("worker Fastify app", () => { expect(workerMongoUri({})).toBe(uri); const mockHttp = await createWorkerApp(); - expect(KeyvMongoMock.instances[0]?.url).toBe(uri); + expect(KeyvMongoMock.instances).toHaveLength(0); const created = await mockHttp.server.inject({ method: "POST", url: "/bins", }); expect(created.statusCode).toBe(200); + expect(KeyvMongoMock.instances).toHaveLength(1); + expect(KeyvMongoMock.instances[0]?.url).toBe(uri); await mockHttp.close(); delete process.env.MONGODB_URI; }); diff --git a/worker/app.ts b/worker/app.ts index cf39b98..bf0e913 100644 --- a/worker/app.ts +++ b/worker/app.ts @@ -2,8 +2,12 @@ import { handleAsNodeRequest } from "cloudflare:node"; import KeyvMongo from "@keyv/mongo"; import Keyv from "keyv"; import { MockHttp, type MockHttpOptions } from "../src/mock-http.js"; -import { MockHttpBins } from "../src/mock-http-bins.js"; +import { + MockHttpBins, + type MockHttpBinsOptions, +} from "../src/mock-http-bins.js"; import { setPublicFileReader } from "../src/public-files.js"; +import { PUBLIC_PROTOCOL_HEADER } from "../src/routes/bins/management.js"; export const WORKER_PORT = 3000; @@ -57,15 +61,57 @@ export function workerMongoUri(env: WorkerEnv = {}): string | undefined { return uri; } +type WorkerBinKeyv = NonNullable; + +/** + * Keyv backed by MongoDB. `@keyv/mongo` calls `MongoClient.connect()` from + * its constructor, and workerd rejects that network I/O during isolate + * startup, so the client is created on the first bin operation. + */ +function createLazyBinKeyv(uri: string): WorkerBinKeyv { + let keyv: WorkerBinKeyv | undefined; + const open = (): WorkerBinKeyv => { + keyv ??= new Keyv({ + store: new KeyvMongo(uri, workerBinMongoOptions), + }) as WorkerBinKeyv; + return keyv; + }; + + return new Proxy({} as WorkerBinKeyv, { + get(_target, property) { + const target = open(); + const value = Reflect.get(target, property, target); + if (typeof value === "function") { + return value.bind(target); + } + return value; + }, + }); +} + /** Request bins stored in MongoDB through Keyv. */ export function createWorkerBins(uri: string): MockHttpBins { return new MockHttpBins({ - keyv: new Keyv({ - store: new KeyvMongo(uri, workerBinMongoOptions), - }), + keyv: createLazyBinKeyv(uri), }); } +/** + * The Node server inside the Worker is plain HTTP. workerd then rewrites + * `x-forwarded-proto` to `http`, so Fastify cannot see the client scheme. + * Stamp it on {@link PUBLIC_PROTOCOL_HEADER}, which the bridge leaves intact. + */ +export function requestWithPublicProtocol(request: Request): Request { + const protocol = new URL(request.url).protocol.slice(0, -1); + if (request.headers.get(PUBLIC_PROTOCOL_HEADER) === protocol) { + return request; + } + + const headers = new Headers(request.headers); + headers.set(PUBLIC_PROTOCOL_HEADER, protocol); + return new Request(request, { headers }); +} + export type WorkerDispatch = ( port: number, request: Request, @@ -155,5 +201,5 @@ export async function handleWorkerFetch( } bindPublicAssets(env.ASSETS); - return dispatch(WORKER_PORT, request); + return dispatch(WORKER_PORT, requestWithPublicProtocol(request)); } diff --git a/worker/cloudflare-workers.d.ts b/worker/cloudflare-workers.d.ts new file mode 100644 index 0000000..e16c7fa --- /dev/null +++ b/worker/cloudflare-workers.d.ts @@ -0,0 +1,3 @@ +declare module "cloudflare:workers" { + export const env: import("./app.js").WorkerEnv; +} diff --git a/worker/index.ts b/worker/index.ts index ee00fd1..494b691 100644 --- a/worker/index.ts +++ b/worker/index.ts @@ -1,3 +1,4 @@ +import { env as workerEnv } from "cloudflare:workers"; import { createWorkerApp, handleWorkerFetch, @@ -5,30 +6,15 @@ import { type WorkerEnv, } from "./app.js"; -let listening: Promise | undefined; - -// The Atlas URI is on the request env, so Fastify starts on the first fetch. -async function ensureWorkerListening(env: WorkerEnv): Promise { - if (!listening) { - const pending = (async () => { - try { - await listenWorkerApp(await createWorkerApp(env)); - } catch (error) { - if (listening === pending) { - listening = undefined; - } - throw error; - } - })(); - listening = pending; - } - - await listening; -} +// Fastify compiles routes with `new Function()`. workerd allows that only +// while the isolate is starting, so the server has to listen at module load. +// `cloudflare:workers` exposes MONGODB_URI here. The Mongo client connects +// later, on the first bin operation, because network I/O is rejected during +// startup. +await listenWorkerApp(await createWorkerApp(workerEnv)); export default { async fetch(request: Request, env: WorkerEnv): Promise { - await ensureWorkerListening(env); return handleWorkerFetch(request, env); }, }; diff --git a/wrangler.test.jsonc b/wrangler.test.jsonc new file mode 100644 index 0000000..d75f9ab --- /dev/null +++ b/wrangler.test.jsonc @@ -0,0 +1,6 @@ +{ + "name": "mockhttp-test", + "main": "worker/index.ts", + "compatibility_date": "2026-08-27", + "compatibility_flags": ["nodejs_compat"] +}