feat: migrate to Hono framework
- Add Hono as HTTP framework for Cloudflare Workers - Create app.ts with declarative routing and middleware - Add hono-adapters.ts for auth, rate limit, request ID middleware - Refactor handlers to use Hono Context signature - Maintain all existing business logic unchanged Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
540
package-lock.json
generated
540
package-lock.json
generated
@@ -7,8 +7,12 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "cloud-instances-api",
|
"name": "cloud-instances-api",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"hono": "^4.11.7"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "^4.20241205.0",
|
"@cloudflare/workers-types": "^4.20241205.0",
|
||||||
|
"tsx": "^4.7.0",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"vitest": "^2.1.8",
|
"vitest": "^2.1.8",
|
||||||
"wrangler": "^4.59.3"
|
"wrangler": "^4.59.3"
|
||||||
@@ -1832,6 +1836,28 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-tsconfig": {
|
||||||
|
"version": "4.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
|
||||||
|
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"resolve-pkg-maps": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hono": {
|
||||||
|
"version": "4.11.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz",
|
||||||
|
"integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/kleur": {
|
"node_modules/kleur": {
|
||||||
"version": "4.1.5",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
||||||
@@ -1967,6 +1993,16 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/resolve-pkg-maps": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.55.3",
|
"version": "4.55.3",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.3.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.3.tgz",
|
||||||
@@ -2166,6 +2202,510 @@
|
|||||||
"license": "0BSD",
|
"license": "0BSD",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/tsx": {
|
||||||
|
"version": "4.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||||
|
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "~0.27.0",
|
||||||
|
"get-tsconfig": "^4.7.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tsx": "dist/cli.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/esbuild": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.27.2",
|
||||||
|
"@esbuild/android-arm": "0.27.2",
|
||||||
|
"@esbuild/android-arm64": "0.27.2",
|
||||||
|
"@esbuild/android-x64": "0.27.2",
|
||||||
|
"@esbuild/darwin-arm64": "0.27.2",
|
||||||
|
"@esbuild/darwin-x64": "0.27.2",
|
||||||
|
"@esbuild/freebsd-arm64": "0.27.2",
|
||||||
|
"@esbuild/freebsd-x64": "0.27.2",
|
||||||
|
"@esbuild/linux-arm": "0.27.2",
|
||||||
|
"@esbuild/linux-arm64": "0.27.2",
|
||||||
|
"@esbuild/linux-ia32": "0.27.2",
|
||||||
|
"@esbuild/linux-loong64": "0.27.2",
|
||||||
|
"@esbuild/linux-mips64el": "0.27.2",
|
||||||
|
"@esbuild/linux-ppc64": "0.27.2",
|
||||||
|
"@esbuild/linux-riscv64": "0.27.2",
|
||||||
|
"@esbuild/linux-s390x": "0.27.2",
|
||||||
|
"@esbuild/linux-x64": "0.27.2",
|
||||||
|
"@esbuild/netbsd-arm64": "0.27.2",
|
||||||
|
"@esbuild/netbsd-x64": "0.27.2",
|
||||||
|
"@esbuild/openbsd-arm64": "0.27.2",
|
||||||
|
"@esbuild/openbsd-x64": "0.27.2",
|
||||||
|
"@esbuild/openharmony-arm64": "0.27.2",
|
||||||
|
"@esbuild/sunos-x64": "0.27.2",
|
||||||
|
"@esbuild/win32-arm64": "0.27.2",
|
||||||
|
"@esbuild/win32-ia32": "0.27.2",
|
||||||
|
"@esbuild/win32-x64": "0.27.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
|||||||
@@ -33,5 +33,8 @@
|
|||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"vitest": "^2.1.8",
|
"vitest": "^2.1.8",
|
||||||
"wrangler": "^4.59.3"
|
"wrangler": "^4.59.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"hono": "^4.11.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
191
src/app.ts
Normal file
191
src/app.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
/**
|
||||||
|
* Hono Application Setup
|
||||||
|
*
|
||||||
|
* Configures Hono app with CORS, security headers, and routes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Hono } from 'hono';
|
||||||
|
import type { Context } from 'hono';
|
||||||
|
import type { Env } from './types';
|
||||||
|
import { CORS, HTTP_STATUS } from './constants';
|
||||||
|
import { createLogger } from './utils/logger';
|
||||||
|
import {
|
||||||
|
requestIdMiddleware,
|
||||||
|
authMiddleware,
|
||||||
|
rateLimitMiddleware,
|
||||||
|
optionalAuthMiddleware,
|
||||||
|
} from './middleware/hono-adapters';
|
||||||
|
import { handleHealth } from './routes/health';
|
||||||
|
import { handleInstances } from './routes/instances';
|
||||||
|
import { handleSync } from './routes/sync';
|
||||||
|
|
||||||
|
const logger = createLogger('[App]');
|
||||||
|
|
||||||
|
// Context variables type
|
||||||
|
type Variables = {
|
||||||
|
requestId: string;
|
||||||
|
authenticated?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create Hono app with type-safe bindings
|
||||||
|
const app = new Hono<{ Bindings: Env; Variables: Variables }>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get CORS origin for request
|
||||||
|
* Reused from original index.ts logic
|
||||||
|
*/
|
||||||
|
function getCorsOrigin(c: Context<{ Bindings: Env; Variables: Variables }>): string {
|
||||||
|
const origin = c.req.header('Origin');
|
||||||
|
const env = c.env;
|
||||||
|
|
||||||
|
// Environment variable has explicit origin configured (highest priority)
|
||||||
|
if (env.CORS_ORIGIN && env.CORS_ORIGIN !== '*') {
|
||||||
|
return env.CORS_ORIGIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build allowed origins list based on environment
|
||||||
|
const isDevelopment = env.ENVIRONMENT === 'development';
|
||||||
|
const allowedOrigins = isDevelopment
|
||||||
|
? [...CORS.ALLOWED_ORIGINS, ...CORS.DEVELOPMENT_ORIGINS]
|
||||||
|
: CORS.ALLOWED_ORIGINS;
|
||||||
|
|
||||||
|
// Request origin is in allowed list
|
||||||
|
if (origin && allowedOrigins.includes(origin)) {
|
||||||
|
return origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log unmatched origins for security monitoring
|
||||||
|
if (origin && !allowedOrigins.includes(origin)) {
|
||||||
|
const sanitizedOrigin = origin.replace(/[\r\n\t]/g, '').substring(0, 256);
|
||||||
|
logger.warn('Unmatched origin - using default', {
|
||||||
|
requested_origin: sanitizedOrigin,
|
||||||
|
environment: env.ENVIRONMENT || 'production',
|
||||||
|
default_origin: CORS.DEFAULT_ORIGIN,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return explicit default (no wildcard)
|
||||||
|
return CORS.DEFAULT_ORIGIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CORS middleware
|
||||||
|
* Configured dynamically based on request origin
|
||||||
|
*/
|
||||||
|
app.use('*', async (c, next) => {
|
||||||
|
// Handle OPTIONS preflight - must come before await next()
|
||||||
|
if (c.req.method === 'OPTIONS') {
|
||||||
|
const origin = getCorsOrigin(c);
|
||||||
|
c.res.headers.set('Access-Control-Allow-Origin', origin);
|
||||||
|
c.res.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||||
|
c.res.headers.set('Access-Control-Allow-Headers', 'Content-Type, X-API-Key');
|
||||||
|
c.res.headers.set('Access-Control-Max-Age', CORS.MAX_AGE);
|
||||||
|
return c.body(null, 204);
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
|
||||||
|
// Set CORS headers after processing
|
||||||
|
const origin = getCorsOrigin(c);
|
||||||
|
c.res.headers.set('Access-Control-Allow-Origin', origin);
|
||||||
|
c.res.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||||
|
c.res.headers.set('Access-Control-Allow-Headers', 'Content-Type, X-API-Key');
|
||||||
|
c.res.headers.set('Access-Control-Max-Age', CORS.MAX_AGE);
|
||||||
|
c.res.headers.set(
|
||||||
|
'Access-Control-Expose-Headers',
|
||||||
|
'X-RateLimit-Retry-After, Retry-After, X-Request-ID'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request ID middleware
|
||||||
|
* Adds unique request ID for tracing
|
||||||
|
*/
|
||||||
|
app.use('*', requestIdMiddleware);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security headers middleware
|
||||||
|
* Applied to all responses
|
||||||
|
*/
|
||||||
|
app.use('*', async (c, next) => {
|
||||||
|
await next();
|
||||||
|
|
||||||
|
// Add security headers to response
|
||||||
|
c.res.headers.set('X-Content-Type-Options', 'nosniff');
|
||||||
|
c.res.headers.set('X-Frame-Options', 'DENY');
|
||||||
|
c.res.headers.set('Strict-Transport-Security', 'max-age=31536000');
|
||||||
|
c.res.headers.set('Content-Security-Policy', "default-src 'none'");
|
||||||
|
c.res.headers.set('X-XSS-Protection', '1; mode=block');
|
||||||
|
c.res.headers.set('Referrer-Policy', 'no-referrer');
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment validation middleware
|
||||||
|
* Checks required environment variables before processing
|
||||||
|
*/
|
||||||
|
app.use('*', async (c, next) => {
|
||||||
|
const required = ['API_KEY'];
|
||||||
|
const missing = required.filter((key) => !c.env[key as keyof Env]);
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
logger.error('Missing required environment variables', {
|
||||||
|
missing,
|
||||||
|
request_id: c.get('requestId'),
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
error: 'Service Unavailable',
|
||||||
|
message: 'Service configuration error',
|
||||||
|
},
|
||||||
|
503
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Routes
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Health check (public endpoint with optional authentication)
|
||||||
|
app.get('/health', optionalAuthMiddleware, handleHealth);
|
||||||
|
|
||||||
|
// Query instances (authenticated, rate limited)
|
||||||
|
app.get('/instances', authMiddleware, rateLimitMiddleware, handleInstances);
|
||||||
|
|
||||||
|
// Sync trigger (authenticated, rate limited)
|
||||||
|
app.post('/sync', authMiddleware, rateLimitMiddleware, handleSync);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 404 handler
|
||||||
|
*/
|
||||||
|
app.notFound((c) => {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
error: 'Not Found',
|
||||||
|
path: c.req.path,
|
||||||
|
},
|
||||||
|
HTTP_STATUS.NOT_FOUND
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global error handler
|
||||||
|
*/
|
||||||
|
app.onError((err, c) => {
|
||||||
|
logger.error('Request error', {
|
||||||
|
error: err,
|
||||||
|
request_id: c.get('requestId'),
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
error: 'Internal Server Error',
|
||||||
|
},
|
||||||
|
HTTP_STATUS.INTERNAL_ERROR
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
190
src/index.ts
190
src/index.ts
@@ -5,199 +5,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Env } from './types';
|
import { Env } from './types';
|
||||||
import { handleSync, handleInstances, handleHealth } from './routes';
|
import app from './app';
|
||||||
import {
|
|
||||||
authenticateRequest,
|
|
||||||
verifyApiKey,
|
|
||||||
createUnauthorizedResponse,
|
|
||||||
checkRateLimit,
|
|
||||||
createRateLimitResponse,
|
|
||||||
} from './middleware';
|
|
||||||
import { CORS, HTTP_STATUS } from './constants';
|
|
||||||
import { createLogger } from './utils/logger';
|
import { createLogger } from './utils/logger';
|
||||||
import { SyncOrchestrator } from './services/sync';
|
import { SyncOrchestrator } from './services/sync';
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate required environment variables
|
|
||||||
*/
|
|
||||||
function validateEnv(env: Env): { valid: boolean; missing: string[] } {
|
|
||||||
const required = ['API_KEY'];
|
|
||||||
const missing = required.filter(key => !env[key as keyof Env]);
|
|
||||||
return { valid: missing.length === 0, missing };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get CORS origin for request
|
|
||||||
*
|
|
||||||
* Security: No wildcard fallback. Returns explicit allowed origin or default.
|
|
||||||
* Logs unmatched origins for monitoring.
|
|
||||||
*/
|
|
||||||
function getCorsOrigin(request: Request, env: Env): string {
|
|
||||||
const origin = request.headers.get('Origin');
|
|
||||||
const logger = createLogger('[CORS]', env);
|
|
||||||
|
|
||||||
// Environment variable has explicit origin configured (highest priority)
|
|
||||||
if (env.CORS_ORIGIN && env.CORS_ORIGIN !== '*') {
|
|
||||||
return env.CORS_ORIGIN;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build allowed origins list based on environment
|
|
||||||
const isDevelopment = env.ENVIRONMENT === 'development';
|
|
||||||
const allowedOrigins = isDevelopment
|
|
||||||
? [...CORS.ALLOWED_ORIGINS, ...CORS.DEVELOPMENT_ORIGINS]
|
|
||||||
: CORS.ALLOWED_ORIGINS;
|
|
||||||
|
|
||||||
// Request origin is in allowed list
|
|
||||||
if (origin && allowedOrigins.includes(origin)) {
|
|
||||||
return origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log unmatched origins for security monitoring
|
|
||||||
if (origin && !allowedOrigins.includes(origin)) {
|
|
||||||
// Sanitize origin to prevent log injection (remove control characters)
|
|
||||||
const sanitizedOrigin = origin.replace(/[\r\n\t]/g, '').substring(0, 256);
|
|
||||||
logger.warn('Unmatched origin - using default', {
|
|
||||||
requested_origin: sanitizedOrigin,
|
|
||||||
environment: env.ENVIRONMENT || 'production',
|
|
||||||
default_origin: CORS.DEFAULT_ORIGIN
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return explicit default (no wildcard)
|
|
||||||
return CORS.DEFAULT_ORIGIN;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add security headers to response
|
|
||||||
* Performance optimization: Reuses response body without cloning to minimize memory allocation
|
|
||||||
*
|
|
||||||
* Benefits:
|
|
||||||
* - Avoids Response.clone() which copies the entire body stream
|
|
||||||
* - Directly references response.body (ReadableStream) without duplication
|
|
||||||
* - Reduces memory allocation and GC pressure per request
|
|
||||||
*
|
|
||||||
* Note: response.body can be null for 204 No Content or empty responses
|
|
||||||
*/
|
|
||||||
function addSecurityHeaders(response: Response, corsOrigin?: string, requestId?: string): Response {
|
|
||||||
const headers = new Headers(response.headers);
|
|
||||||
|
|
||||||
// Basic security headers
|
|
||||||
headers.set('X-Content-Type-Options', 'nosniff');
|
|
||||||
headers.set('X-Frame-Options', 'DENY');
|
|
||||||
headers.set('Strict-Transport-Security', 'max-age=31536000');
|
|
||||||
|
|
||||||
// CORS headers
|
|
||||||
headers.set('Access-Control-Allow-Origin', corsOrigin || CORS.DEFAULT_ORIGIN);
|
|
||||||
headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
||||||
headers.set('Access-Control-Allow-Headers', 'Content-Type, X-API-Key');
|
|
||||||
headers.set('Access-Control-Max-Age', CORS.MAX_AGE);
|
|
||||||
headers.set('Access-Control-Expose-Headers', 'X-RateLimit-Retry-After, Retry-After, X-Request-ID');
|
|
||||||
|
|
||||||
// Additional security headers
|
|
||||||
headers.set('Content-Security-Policy', "default-src 'none'");
|
|
||||||
headers.set('X-XSS-Protection', '1; mode=block');
|
|
||||||
headers.set('Referrer-Policy', 'no-referrer');
|
|
||||||
|
|
||||||
// Request ID for audit trail
|
|
||||||
if (requestId) {
|
|
||||||
headers.set('X-Request-ID', requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new Response with same body reference (no copy) and updated headers
|
|
||||||
return new Response(response.body, {
|
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
* HTTP Request Handler
|
* HTTP Request Handler (delegated to Hono)
|
||||||
*/
|
*/
|
||||||
async fetch(request: Request, env: Env, _ctx: ExecutionContext): Promise<Response> {
|
fetch: app.fetch,
|
||||||
const url = new URL(request.url);
|
|
||||||
const path = url.pathname;
|
|
||||||
|
|
||||||
// Generate request ID for audit trail (use CF-Ray if available, otherwise generate UUID)
|
|
||||||
const requestId = request.headers.get('CF-Ray') || crypto.randomUUID();
|
|
||||||
|
|
||||||
// Get CORS origin based on request and configuration
|
|
||||||
const corsOrigin = getCorsOrigin(request, env);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Handle OPTIONS preflight requests
|
|
||||||
if (request.method === 'OPTIONS') {
|
|
||||||
return addSecurityHeaders(new Response(null, { status: 204 }), corsOrigin, requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate required environment variables
|
|
||||||
const envValidation = validateEnv(env);
|
|
||||||
if (!envValidation.valid) {
|
|
||||||
const logger = createLogger('[Worker]');
|
|
||||||
logger.error('Missing required environment variables', { missing: envValidation.missing, request_id: requestId });
|
|
||||||
return addSecurityHeaders(
|
|
||||||
Response.json(
|
|
||||||
{ error: 'Service Unavailable', message: 'Service configuration error' },
|
|
||||||
{ status: 503 }
|
|
||||||
),
|
|
||||||
corsOrigin,
|
|
||||||
requestId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Health check (public endpoint with optional authentication)
|
|
||||||
if (path === '/health') {
|
|
||||||
const apiKey = request.headers.get('X-API-Key');
|
|
||||||
const authenticated = apiKey ? verifyApiKey(apiKey, env) : false;
|
|
||||||
return addSecurityHeaders(await handleHealth(env, authenticated), corsOrigin, requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authentication required for all other endpoints
|
|
||||||
const isAuthenticated = await authenticateRequest(request, env);
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
return addSecurityHeaders(createUnauthorizedResponse(), corsOrigin, requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rate limiting for authenticated endpoints
|
|
||||||
const rateLimitCheck = await checkRateLimit(request, path, env);
|
|
||||||
if (!rateLimitCheck.allowed) {
|
|
||||||
return addSecurityHeaders(createRateLimitResponse(rateLimitCheck.retryAfter!), corsOrigin, requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query instances
|
|
||||||
if (path === '/instances' && request.method === 'GET') {
|
|
||||||
return addSecurityHeaders(await handleInstances(request, env), corsOrigin, requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync trigger
|
|
||||||
if (path === '/sync' && request.method === 'POST') {
|
|
||||||
return addSecurityHeaders(await handleSync(request, env), corsOrigin, requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 404 Not Found
|
|
||||||
return addSecurityHeaders(
|
|
||||||
Response.json(
|
|
||||||
{ error: 'Not Found', path },
|
|
||||||
{ status: HTTP_STATUS.NOT_FOUND }
|
|
||||||
),
|
|
||||||
corsOrigin,
|
|
||||||
requestId
|
|
||||||
);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
const logger = createLogger('[Worker]');
|
|
||||||
logger.error('Request error', { error, request_id: requestId });
|
|
||||||
return addSecurityHeaders(
|
|
||||||
Response.json(
|
|
||||||
{ error: 'Internal Server Error' },
|
|
||||||
{ status: HTTP_STATUS.INTERNAL_ERROR }
|
|
||||||
),
|
|
||||||
corsOrigin,
|
|
||||||
requestId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scheduled (Cron) Handler
|
* Scheduled (Cron) Handler
|
||||||
|
|||||||
110
src/middleware/hono-adapters.ts
Normal file
110
src/middleware/hono-adapters.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* Hono Middleware Adapters
|
||||||
|
*
|
||||||
|
* Adapts existing authentication and rate limiting middleware to Hono's middleware pattern.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Context, Next } from 'hono';
|
||||||
|
import type { Env } from '../types';
|
||||||
|
import {
|
||||||
|
authenticateRequest,
|
||||||
|
verifyApiKey,
|
||||||
|
createUnauthorizedResponse,
|
||||||
|
} from './auth';
|
||||||
|
import { checkRateLimit, createRateLimitResponse } from './rateLimit';
|
||||||
|
import { createLogger } from '../utils/logger';
|
||||||
|
|
||||||
|
const logger = createLogger('[Middleware]');
|
||||||
|
|
||||||
|
// Context variables type
|
||||||
|
type Variables = {
|
||||||
|
requestId: string;
|
||||||
|
authenticated?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request ID middleware
|
||||||
|
* Adds unique request ID to context for tracing
|
||||||
|
*/
|
||||||
|
export async function requestIdMiddleware(
|
||||||
|
c: Context<{ Bindings: Env; Variables: Variables }>,
|
||||||
|
next: Next
|
||||||
|
): Promise<void> {
|
||||||
|
// Use CF-Ray if available, otherwise generate UUID
|
||||||
|
const requestId = c.req.header('CF-Ray') || crypto.randomUUID();
|
||||||
|
|
||||||
|
// Store in context for handlers to use
|
||||||
|
c.set('requestId', requestId);
|
||||||
|
|
||||||
|
await next();
|
||||||
|
|
||||||
|
// Add to response headers
|
||||||
|
c.res.headers.set('X-Request-ID', requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication middleware
|
||||||
|
* Validates X-API-Key header using existing auth logic
|
||||||
|
*/
|
||||||
|
export async function authMiddleware(
|
||||||
|
c: Context<{ Bindings: Env; Variables: Variables }>,
|
||||||
|
next: Next
|
||||||
|
): Promise<Response | void> {
|
||||||
|
const request = c.req.raw;
|
||||||
|
const env = c.env;
|
||||||
|
|
||||||
|
const isAuthenticated = await authenticateRequest(request, env);
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
logger.warn('[Auth] Unauthorized request', {
|
||||||
|
path: c.req.path,
|
||||||
|
requestId: c.get('requestId'),
|
||||||
|
});
|
||||||
|
return createUnauthorizedResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rate limiting middleware
|
||||||
|
* Applies rate limits based on endpoint using existing rate limit logic
|
||||||
|
*/
|
||||||
|
export async function rateLimitMiddleware(
|
||||||
|
c: Context<{ Bindings: Env; Variables: Variables }>,
|
||||||
|
next: Next
|
||||||
|
): Promise<Response | void> {
|
||||||
|
const request = c.req.raw;
|
||||||
|
const path = c.req.path;
|
||||||
|
const env = c.env;
|
||||||
|
|
||||||
|
const rateLimitCheck = await checkRateLimit(request, path, env);
|
||||||
|
|
||||||
|
if (!rateLimitCheck.allowed) {
|
||||||
|
logger.warn('[RateLimit] Rate limit exceeded', {
|
||||||
|
path,
|
||||||
|
retryAfter: rateLimitCheck.retryAfter,
|
||||||
|
requestId: c.get('requestId'),
|
||||||
|
});
|
||||||
|
return createRateLimitResponse(rateLimitCheck.retryAfter!);
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional authentication middleware for health check
|
||||||
|
* Checks if API key is provided and valid, stores result in context
|
||||||
|
*/
|
||||||
|
export async function optionalAuthMiddleware(
|
||||||
|
c: Context<{ Bindings: Env; Variables: Variables }>,
|
||||||
|
next: Next
|
||||||
|
): Promise<void> {
|
||||||
|
const apiKey = c.req.header('X-API-Key');
|
||||||
|
const authenticated = apiKey ? verifyApiKey(apiKey, c.env) : false;
|
||||||
|
|
||||||
|
// Store authentication status in context
|
||||||
|
c.set('authenticated', authenticated);
|
||||||
|
|
||||||
|
await next();
|
||||||
|
}
|
||||||
@@ -3,12 +3,19 @@
|
|||||||
* Comprehensive health monitoring for database and provider sync status
|
* Comprehensive health monitoring for database and provider sync status
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Context } from 'hono';
|
||||||
import { Env } from '../types';
|
import { Env } from '../types';
|
||||||
import { HTTP_STATUS } from '../constants';
|
import { HTTP_STATUS } from '../constants';
|
||||||
import { createLogger } from '../utils/logger';
|
import { createLogger } from '../utils/logger';
|
||||||
|
|
||||||
const logger = createLogger('[Health]');
|
const logger = createLogger('[Health]');
|
||||||
|
|
||||||
|
// Context variables type
|
||||||
|
type Variables = {
|
||||||
|
requestId: string;
|
||||||
|
authenticated?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component health status
|
* Component health status
|
||||||
*/
|
*/
|
||||||
@@ -159,18 +166,17 @@ function sanitizeError(error: string): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle health check request
|
* Handle health check request
|
||||||
* @param env - Cloudflare Worker environment
|
* @param c - Hono context
|
||||||
* @param authenticated - Whether the request is authenticated (default: false)
|
|
||||||
*/
|
*/
|
||||||
export async function handleHealth(
|
export async function handleHealth(
|
||||||
env: Env,
|
c: Context<{ Bindings: Env; Variables: Variables }>
|
||||||
authenticated: boolean = false
|
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
|
const authenticated = c.get('authenticated') ?? false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check database health
|
// Check database health
|
||||||
const dbHealth = await checkDatabaseHealth(env.DB);
|
const dbHealth = await checkDatabaseHealth(c.env.DB);
|
||||||
|
|
||||||
// If database is unhealthy, return early
|
// If database is unhealthy, return early
|
||||||
if (dbHealth.status === 'unhealthy') {
|
if (dbHealth.status === 'unhealthy') {
|
||||||
@@ -206,7 +212,7 @@ export async function handleHealth(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get all providers with aggregated counts in a single query
|
// Get all providers with aggregated counts in a single query
|
||||||
const providersWithCounts = await env.DB.prepare(`
|
const providersWithCounts = await c.env.DB.prepare(`
|
||||||
SELECT
|
SELECT
|
||||||
p.id,
|
p.id,
|
||||||
p.name,
|
p.name,
|
||||||
|
|||||||
@@ -5,10 +5,17 @@
|
|||||||
* Integrates with cache service for performance optimization.
|
* Integrates with cache service for performance optimization.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Context } from 'hono';
|
||||||
import type { Env, InstanceQueryParams } from '../types';
|
import type { Env, InstanceQueryParams } from '../types';
|
||||||
import { QueryService } from '../services/query';
|
import { QueryService } from '../services/query';
|
||||||
import { getGlobalCacheService } from '../services/cache';
|
import { getGlobalCacheService } from '../services/cache';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
|
// Context variables type
|
||||||
|
type Variables = {
|
||||||
|
requestId: string;
|
||||||
|
authenticated?: boolean;
|
||||||
|
};
|
||||||
import {
|
import {
|
||||||
SUPPORTED_PROVIDERS,
|
SUPPORTED_PROVIDERS,
|
||||||
type SupportedProvider,
|
type SupportedProvider,
|
||||||
@@ -311,24 +318,22 @@ function parseQueryParams(url: URL): {
|
|||||||
/**
|
/**
|
||||||
* Handle GET /instances endpoint
|
* Handle GET /instances endpoint
|
||||||
*
|
*
|
||||||
* @param request - HTTP request object
|
* @param c - Hono context
|
||||||
* @param env - Cloudflare Worker environment bindings
|
|
||||||
* @returns JSON response with instance query results
|
* @returns JSON response with instance query results
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* GET /instances?provider=linode&min_vcpu=2&max_price=20&sort_by=price&order=asc&limit=50
|
* GET /instances?provider=linode&min_vcpu=2&max_price=20&sort_by=price&order=asc&limit=50
|
||||||
*/
|
*/
|
||||||
export async function handleInstances(
|
export async function handleInstances(
|
||||||
request: Request,
|
c: Context<{ Bindings: Env; Variables: Variables }>
|
||||||
env: Env
|
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
logger.info('[Instances] Request received', { url: request.url });
|
logger.info('[Instances] Request received', { url: c.req.url });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Parse URL and query parameters
|
// Parse URL and query parameters
|
||||||
const url = new URL(request.url);
|
const url = new URL(c.req.url);
|
||||||
const parseResult = parseQueryParams(url);
|
const parseResult = parseQueryParams(url);
|
||||||
|
|
||||||
// Handle validation errors
|
// Handle validation errors
|
||||||
@@ -347,7 +352,7 @@ export async function handleInstances(
|
|||||||
logger.info('[Instances] Query params validated', params as unknown as Record<string, unknown>);
|
logger.info('[Instances] Query params validated', params as unknown as Record<string, unknown>);
|
||||||
|
|
||||||
// Get global cache service singleton (shared across all routes)
|
// Get global cache service singleton (shared across all routes)
|
||||||
const cacheService = getGlobalCacheService(CACHE_TTL.INSTANCES, env.RATE_LIMIT_KV);
|
const cacheService = getGlobalCacheService(CACHE_TTL.INSTANCES, c.env.RATE_LIMIT_KV);
|
||||||
|
|
||||||
// Generate cache key from query parameters
|
// Generate cache key from query parameters
|
||||||
const cacheKey = cacheService.generateKey(params as unknown as Record<string, unknown>);
|
const cacheKey = cacheService.generateKey(params as unknown as Record<string, unknown>);
|
||||||
@@ -421,7 +426,7 @@ export async function handleInstances(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Get QueryService singleton (reused across requests)
|
// Get QueryService singleton (reused across requests)
|
||||||
const queryService = getQueryService(env.DB, env);
|
const queryService = getQueryService(c.env.DB, c.env);
|
||||||
const result = await queryService.queryInstances(queryParams);
|
const result = await queryService.queryInstances(queryParams);
|
||||||
|
|
||||||
const queryTime = Date.now() - startTime;
|
const queryTime = Date.now() - startTime;
|
||||||
|
|||||||
@@ -5,12 +5,19 @@
|
|||||||
* Validates request parameters and orchestrates sync operations.
|
* Validates request parameters and orchestrates sync operations.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Context } from 'hono';
|
||||||
import type { Env } from '../types';
|
import type { Env } from '../types';
|
||||||
import { SyncOrchestrator } from '../services/sync';
|
import { SyncOrchestrator } from '../services/sync';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { SUPPORTED_PROVIDERS, HTTP_STATUS } from '../constants';
|
import { SUPPORTED_PROVIDERS, HTTP_STATUS } from '../constants';
|
||||||
import { parseJsonBody, validateProviders, createErrorResponse } from '../utils/validation';
|
import { parseJsonBody, validateProviders, createErrorResponse } from '../utils/validation';
|
||||||
|
|
||||||
|
// Context variables type
|
||||||
|
type Variables = {
|
||||||
|
requestId: string;
|
||||||
|
authenticated?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request body interface for sync endpoint
|
* Request body interface for sync endpoint
|
||||||
*/
|
*/
|
||||||
@@ -23,8 +30,7 @@ interface SyncRequestBody {
|
|||||||
/**
|
/**
|
||||||
* Handle POST /sync endpoint
|
* Handle POST /sync endpoint
|
||||||
*
|
*
|
||||||
* @param request - HTTP request object
|
* @param c - Hono context
|
||||||
* @param env - Cloudflare Worker environment bindings
|
|
||||||
* @returns JSON response with sync results
|
* @returns JSON response with sync results
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
@@ -35,8 +41,7 @@ interface SyncRequestBody {
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export async function handleSync(
|
export async function handleSync(
|
||||||
request: Request,
|
c: Context<{ Bindings: Env; Variables: Variables }>
|
||||||
env: Env
|
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const startedAt = new Date().toISOString();
|
const startedAt = new Date().toISOString();
|
||||||
@@ -45,7 +50,7 @@ export async function handleSync(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Validate content-length before parsing body
|
// Validate content-length before parsing body
|
||||||
const contentLength = request.headers.get('content-length');
|
const contentLength = c.req.header('content-length');
|
||||||
if (contentLength) {
|
if (contentLength) {
|
||||||
const bodySize = parseInt(contentLength, 10);
|
const bodySize = parseInt(contentLength, 10);
|
||||||
if (isNaN(bodySize) || bodySize > 10240) { // 10KB limit for sync
|
if (isNaN(bodySize) || bodySize > 10240) { // 10KB limit for sync
|
||||||
@@ -57,12 +62,12 @@ export async function handleSync(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse and validate request body
|
// Parse and validate request body
|
||||||
const contentType = request.headers.get('content-type');
|
const contentType = c.req.header('content-type');
|
||||||
let body: SyncRequestBody = {};
|
let body: SyncRequestBody = {};
|
||||||
|
|
||||||
// Only parse JSON if content-type is set
|
// Only parse JSON if content-type is set
|
||||||
if (contentType && contentType.includes('application/json')) {
|
if (contentType && contentType.includes('application/json')) {
|
||||||
const parseResult = await parseJsonBody<SyncRequestBody>(request);
|
const parseResult = await parseJsonBody<SyncRequestBody>(c.req.raw);
|
||||||
if (!parseResult.success) {
|
if (!parseResult.success) {
|
||||||
logger.error('[Sync] Invalid JSON in request body', {
|
logger.error('[Sync] Invalid JSON in request body', {
|
||||||
code: parseResult.error.code,
|
code: parseResult.error.code,
|
||||||
@@ -90,7 +95,7 @@ export async function handleSync(
|
|||||||
logger.info('[Sync] Validation passed', { providers, force });
|
logger.info('[Sync] Validation passed', { providers, force });
|
||||||
|
|
||||||
// Initialize SyncOrchestrator
|
// Initialize SyncOrchestrator
|
||||||
const orchestrator = new SyncOrchestrator(env.DB, env);
|
const orchestrator = new SyncOrchestrator(c.env.DB, c.env);
|
||||||
|
|
||||||
// Execute synchronization
|
// Execute synchronization
|
||||||
logger.info('[Sync] Starting synchronization', { providers });
|
logger.info('[Sync] Starting synchronization', { providers });
|
||||||
|
|||||||
Reference in New Issue
Block a user