diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..3196bd3 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,1499 @@ +{ + "name": "app-s01qyw", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "app-s01qyw", + "dependencies": { + "hono": "latest" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.0.0", + "typescript": "^5.0.0", + "wrangler": "^4.129.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.5.0", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.16.2", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": ">1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260926.1", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20260702.1", + "dev": true, + "license": "MIT OR Apache-2.0" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.4", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.4", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.24", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "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" + } + }, + "node_modules/esbuild/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/hono": { + "version": "4.13.11", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.11.tgz", + "integrity": "sha512-/SMX/RQNJn7oNmFwH6DtwDqcrzZU2otm1FD6OJe2cWF6cfy/F8hq0XVBv7xW3FTJshRQwuBnXHDq2kNsdZnshg==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/miniflare": { + "version": "5.20260926.0-alpha", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.35.4", + "undici": "7.29.0", + "workerd": "1.20260926.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.4", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/sharp/node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.3" + } + }, + "node_modules/sharp/node_modules/@img/sharp-darwin-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/sharp/node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-linux-arm": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.3" + } + }, + "node_modules/sharp/node_modules/@img/sharp-linux-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.3" + } + }, + "node_modules/sharp/node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" + } + }, + "node_modules/sharp/node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" + } + }, + "node_modules/sharp/node_modules/@img/sharp-linux-s390x": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.3" + } + }, + "node_modules/sharp/node_modules/@img/sharp-linux-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.3" + } + }, + "node_modules/sharp/node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + } + }, + "node_modules/sharp/node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + } + }, + "node_modules/sharp/node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/@img/sharp-win32-ia32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/workerd": { + "version": "1.20260926.1", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260926.1", + "@cloudflare/workerd-darwin-arm64": "1.20260926.1", + "@cloudflare/workerd-linux-64": "1.20260926.1", + "@cloudflare/workerd-linux-arm64": "1.20260926.1", + "@cloudflare/workerd-windows-64": "1.20260926.1" + } + }, + "node_modules/workerd/node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260926.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260926.1.tgz", + "integrity": "sha512-VCIpwgJu5Dm1o+VHLclNOKIdpttEqss7oDHS5DYxgVoonM9Dxxxr1xCQvJbh6IXFuQ1DDrbm5krjte5NsTgX3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/workerd/node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260926.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260926.1.tgz", + "integrity": "sha512-oVuLXfCnr1Xu9sBMNYrEPcFZQNMcviNcacz9l+oqQiLHc5UrVp++lpKbsuWstCfh+/c39Pp37TVeaNBLA8+ReA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/workerd/node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260926.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260926.1.tgz", + "integrity": "sha512-BjmiDvYBVBG5248gCHSZS3yONbcm3/3qKTu9RaQA8o7jQHJfXx9JnWbhkld2KsbXnRGEGHFwpLLidIpIGtQYUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/workerd/node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260926.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260926.1.tgz", + "integrity": "sha512-28bdfQDIFcpaKXMPisADy8BchpIsX+tEAhD+bhfEBXukFGFr8F3ouyiG3HzN3CqAh/OYvifJizzFZtptUPCy7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler": { + "version": "4.143.0", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.5.0", + "@cloudflare/unenv-preset": "2.16.2", + "blake3-wasm": "2.1.5", + "esbuild": "0.28.1", + "miniflare": "5.20260926.0-alpha", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260926.1" + }, + "bin": { + "cf-wrangler": "bin/cf-wrangler.js", + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=22.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^5.20260926.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/wrangler/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "dev": true, + "license": "MIT", + "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 + } + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + } + } +} diff --git a/backend/src/index.ts b/backend/src/index.ts new file mode 100644 index 0000000..36682c5 --- /dev/null +++ b/backend/src/index.ts @@ -0,0 +1,21 @@ +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { AppContext } from './utils'; +import publicRoutes from './routes/public'; +import adminRoutes from './routes/admin'; +import { agentic } from './routes/external'; + +const app = new Hono(); + +app.use('*', cors({ + origin: '*', + allowHeaders: ['Content-Type', 'Authorization', 'X-MAuth-Token', 'x-mworld-origin', 'mAuthAgenticToken'], + allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] +})); + +app.route('/', agentic); + +app.route('/api', publicRoutes); +app.route('/api/admin', adminRoutes); + +export default app; \ No newline at end of file diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts new file mode 100644 index 0000000..1d119b3 --- /dev/null +++ b/backend/src/routes/admin.ts @@ -0,0 +1,68 @@ +import { Hono } from 'hono'; +import { AppContext, adminAuthMiddleware } from '../utils'; + +const adminRoutes = new Hono(); + +adminRoutes.use('*', adminAuthMiddleware); + +adminRoutes.get('/verify-token', (c) => { + return c.json({ success: true, valid: true }); +}); + +adminRoutes.get('/users', async (c) => { + const { results } = await c.env.DB.prepare('SELECT * FROM users ORDER BY created_at DESC').all(); + return c.json({ success: true, users: results || [] }); +}); + +adminRoutes.get('/stats', async (c) => { + const totalUsers = await c.env.DB.prepare('SELECT COUNT(*) as cnt FROM users').first(); + const activeDrivers = await c.env.DB.prepare('SELECT COUNT(*) as cnt FROM driver_profiles WHERE is_available = 1').first(); + const totalRides = await c.env.DB.prepare('SELECT COUNT(*) as cnt FROM rides').first(); + const activeRides = await c.env.DB.prepare("SELECT COUNT(*) as cnt FROM rides WHERE status IN ('requested', 'accepted', 'picked_up')").first(); + const completedRides = await c.env.DB.prepare("SELECT COUNT(*) as cnt FROM rides WHERE status = 'completed'").first(); + + return c.json({ + success: true, + stats: { + total_users: totalUsers?.cnt || 0, + active_drivers: activeDrivers?.cnt || 0, + total_rides: totalRides?.cnt || 0, + active_rides: activeRides?.cnt || 0, + completed_rides: completedRides?.cnt || 0 + } + }); +}); + +adminRoutes.get('/rides', async (c) => { + const { results } = await c.env.DB.prepare(` + SELECT r.*, + p.full_name as passenger_name, p.phone as passenger_phone, + d.full_name as driver_name, d.phone as driver_phone + FROM rides r + LEFT JOIN users p ON r.passenger_email = p.email + LEFT JOIN users d ON r.driver_email = d.email + ORDER BY r.created_at DESC + `).all(); + + return c.json({ success: true, rides: results || [] }); +}); + + +adminRoutes.post('/upload', async (c) => { + try { + const body = await c.req.parseBody(); + const file = body['file']; + if (!file || !(file instanceof File)) { + return c.json({ success: false, msg: 'No file provided' }, 400); + } + const ext = file.name.split('.').pop() || 'bin'; + const key = crypto.randomUUID() + '.' + ext; + await c.env.R2.put(key, await file.arrayBuffer(), { + httpMetadata: { contentType: file.type } + }); + return c.json({ success: true, url: '/api/media/' + key }); + } catch (error: any) { + return c.json({ success: false, msg: error.message }, 500); + } +}); +export default adminRoutes; \ No newline at end of file diff --git a/backend/src/routes/external.ts b/backend/src/routes/external.ts new file mode 100644 index 0000000..3b33539 --- /dev/null +++ b/backend/src/routes/external.ts @@ -0,0 +1,205 @@ +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { AppContext } from '../utils'; + +// ============================================================ +// mAgenticAuth — MirrorWorld system-level auth for skills. +// Token passed as ?mAgenticAuthToken=... query param. +// The valid token is injected via env: c.env.M_AGENTIC_AUTH_TOKEN +// End users never see or use these routes. +// ============================================================ +function verifyAgenticToken(c: any): boolean { + const provided = c.req.query('mAgenticAuthToken') || c.req.query('mAuthAgenticToken'); + const expected = c.env.M_AGENTIC_AUTH_TOKEN; + if (!expected || !provided) return false; + return decodeURIComponent(provided) === decodeURIComponent(expected); +} + +function agenticGuard(c: any): Response | null { + if (!verifyAgenticToken(c)) { + return c.json({ success: false, error: 'Invalid or missing mAgenticAuthToken' }, 401); + } + return null; +} + +// MirrorWorld standard response format +function mwResponse(action: string, logs: string[], data: any) { + const now = () => new Date().toISOString(); + return { + success: true, + response: [ + { level: 'info', message: `Direct action request to /${action}`, timestamp: now() }, + ...logs.map(msg => ({ level: 'info', message: msg, timestamp: now() })), + ], + data, + }; +} + +// Discover all real business-data tables (exclude system/internal tables) +async function discoverTables(db: any): Promise { + const BLOCKED = new Set([ + 'users', 'api_keys', 'webhooks', 'webhook_deliveries', + 'sqlite_master', 'sqlite_sequence', 'notifications', + 'follows', 'profile_activity', 'changelogs', 'settings', 'signaling', + ]); + const res = await db.prepare( + `SELECT name FROM sqlite_master + WHERE type='table' + AND name NOT LIKE 'sqlite_%' + AND name NOT LIKE '%_fts%' + ORDER BY name` + ).all(); + return res.results.map((t: any) => t.name).filter((n: string) => !BLOCKED.has(n)); +} + +async function isValidEntity(db: any, entity: string): Promise { + if (!/^[a-z_][a-z0-9_]*$/i.test(entity)) return false; + const tables = await discoverTables(db); + return tables.includes(entity); +} + +export const agentic = new Hono(); +agentic.use('*', cors({ origin: '*', allowMethods: ['GET', 'POST', 'OPTIONS'], allowHeaders: ['*'] })); + +// ============================================================ +// RESERVED ENDPOINT: /mcp +// Returns the manifest of callable actions. Auto-generated +// from the app's real tables — fully generic, no hardcoding. +// ============================================================ +agentic.get('/mcp', async (c) => { + const denied = agenticGuard(c); if (denied) return denied; + + const tables = await discoverTables(c.env.DB); + const siteName = 'Generated App'; + const siteDesc = 'Execute operations on this application through a single manifest-driven endpoint.'; + + const paths: Record = {}; + + // Generate list/create/update/delete actions for EVERY real entity + for (const t of tables) { + paths[`/list_${t}`] = { + description: `List all records in the "${t}" entity.`, + post: { requestBody: { + prompt: { type: 'string', description: 'Natural-language instruction for the operation.', required: true }, + limit: { type: 'number', description: 'Max records to return (default 50).', required: false }, + page: { type: 'number', description: 'Page number.', required: false }, + } }, + }; + paths[`/create_${t}`] = { + description: `Create a new record in the "${t}" entity. Pass the record fields in the body.`, + post: { requestBody: { + prompt: { type: 'string', description: 'Natural-language instruction for the operation.', required: true }, + data: { type: 'object', description: `The ${t} record fields to insert.`, required: false }, + } }, + }; + paths[`/update_${t}`] = { + description: `Update an existing record in the "${t}" entity by id.`, + post: { requestBody: { + prompt: { type: 'string', description: 'Natural-language instruction for the operation.', required: true }, + id: { type: 'string', description: 'The record id to update.', required: true }, + data: { type: 'object', description: 'Fields to update.', required: false }, + } }, + }; + paths[`/delete_${t}`] = { + description: `Delete a record from the "${t}" entity by id.`, + post: { requestBody: { + prompt: { type: 'string', description: 'Natural-language instruction for the operation.', required: true }, + id: { type: 'string', description: 'The record id to delete.', required: true }, + } }, + }; + } + + return c.json({ title: `${siteName} MCP`, description: siteDesc, paths }); +}); + +// ============================================================ +// GENERIC ACTION HANDLER +// Handles /list_X, /create_X, /update_X, /delete_X for any entity, +// plus /get_app_details and /get_daily_report. +// Injected data goes into the REAL tables → appears as normal app data. +// ============================================================ +const handleAction = async (c: any, action: string) => { + const denied = agenticGuard(c); if (denied) return denied; + + const clean = action.replace(/^\//, ''); + const body = await c.req.json().catch(() => ({})); + + // ── LIST ── + const listM = clean.match(/^list_(.+)$/); + if (listM) { + const entity = listM[1]; + if (!(await isValidEntity(c.env.DB, entity))) { + return c.json({ success: false, error: `Unknown entity: ${entity}` }, 404); + } + const limit = Math.min(100, Number(body.limit) || 50); + const page = Math.max(1, Number(body.page) || 1); + const rows = await c.env.DB.prepare( + `SELECT * FROM ${entity} ORDER BY created_at DESC LIMIT ? OFFSET ?` + ).bind(limit, (page - 1) * limit).all().catch(() => ({ results: [] })); + return c.json(mwResponse(clean, [`Fetched ${rows.results.length} records from "${entity}"`], { + [entity]: rows.results, + count: rows.results.length, + })); + } + + // ── CREATE (inject data — appears as normal app data) ── + const createM = clean.match(/^create_(.+)$/); + if (createM) { + const entity = createM[1]; + if (!(await isValidEntity(c.env.DB, entity))) { + return c.json({ success: false, error: `Unknown entity: ${entity}` }, 404); + } + const record = { ...(body.data || body) }; + delete record.prompt; + if (!record.id) record.id = crypto.randomUUID(); + if (!record.created_at) record.created_at = new Date().toISOString().replace('T', ' ').slice(0, 19); + + const cols = Object.keys(record).filter(k => /^[a-z_][a-z0-9_]*$/i.test(k)); + if (cols.length) { + const placeholders = cols.map(() => '?').join(', '); + await c.env.DB.prepare( + `INSERT INTO ${entity} (${cols.join(', ')}) VALUES (${placeholders})` + ).bind(...cols.map(k => record[k])).run().catch((e: any) => { throw e; }); + } + return c.json(mwResponse(clean, [`Injected new record into "${entity}"`], record)); + } + + // ── UPDATE ── + const updateM = clean.match(/^update_(.+)$/); + if (updateM) { + const entity = updateM[1]; + if (!(await isValidEntity(c.env.DB, entity))) { + return c.json({ success: false, error: `Unknown entity: ${entity}` }, 404); + } + const id = body.id; + if (!id) return c.json({ success: false, error: 'id is required' }, 400); + const data = { ...(body.data || {}) }; + delete data.id; delete data.prompt; + const cols = Object.keys(data).filter(k => /^[a-z_][a-z0-9_]*$/i.test(k)); + if (!cols.length) return c.json({ success: false, error: 'No fields to update' }, 400); + await c.env.DB.prepare( + `UPDATE ${entity} SET ${cols.map(k => `${k} = ?`).join(', ')} WHERE id = ?` + ).bind(...cols.map(k => data[k]), id).run().catch(() => {}); + const updated = await c.env.DB.prepare(`SELECT * FROM ${entity} WHERE id = ?`).bind(id).first(); + return c.json(mwResponse(clean, [`Updated record ${id} in "${entity}"`], updated)); + } + + // ── DELETE ── + const deleteM = clean.match(/^delete_(.+)$/); + if (deleteM) { + const entity = deleteM[1]; + if (!(await isValidEntity(c.env.DB, entity))) { + return c.json({ success: false, error: `Unknown entity: ${entity}` }, 404); + } + const id = body.id; + if (!id) return c.json({ success: false, error: 'id is required' }, 400); + await c.env.DB.prepare(`DELETE FROM ${entity} WHERE id = ?`).bind(id).run().catch(() => {}); + return c.json(mwResponse(clean, [`Deleted record ${id} from "${entity}"`], { id, status: 'deleted' })); + } + + return c.json(mwResponse(clean, [`Unknown action "${clean}"`], { status: 'noop' })); +}; + +// All actions accept both GET and POST, token via query param +agentic.post('/:action', (c) => handleAction(c, c.req.param('action'))); +agentic.get('/:action', (c) => handleAction(c, c.req.param('action'))); \ No newline at end of file diff --git a/backend/src/routes/public.ts b/backend/src/routes/public.ts new file mode 100644 index 0000000..671cdcf --- /dev/null +++ b/backend/src/routes/public.ts @@ -0,0 +1,694 @@ +import { Hono } from 'hono'; +import { AppContext, authMiddleware, getMWorldOrigin, verifyMAuthToken, UserData } from '../utils'; + +const publicRoutes = new Hono(); +publicRoutes.get('/media/*', async (c) => { + const encodedKey = c.req.path.split('/media/')[1] || ''; + const key = decodeURIComponent(encodedKey); + if (!key) return c.json({ success: false, msg: 'Not found' }, 404); + + const object = await c.env.R2.get(key); + if (!object) return c.json({ success: false, msg: 'Not found' }, 404); + + const headers = new Headers(); + object.writeHttpMetadata(headers); + headers.set('etag', object.httpEtag); + headers.set('Accept-Ranges', 'bytes'); + headers.set('Cache-Control', 'public, max-age=31536000'); + + const rangeHeader = c.req.header('Range'); + if (rangeHeader) { + const parts = rangeHeader.replace(/bytes=/, '').split('-'); + const start = parseInt(parts[0], 10); + const end = parts[1] ? parseInt(parts[1], 10) : object.size - 1; + if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || end < start || start >= object.size || end >= object.size) { + return new Response('Requested Range Not Satisfiable', { status: 416 }); + } + headers.set('Content-Range', `bytes ${start}-${end}/${object.size}`); + headers.set('Content-Length', (end - start + 1).toString()); + const rangeObject = await c.env.R2.get(key, { range: { offset: start, length: end - start + 1 } }); + if (!rangeObject) return c.json({ success: false, msg: 'Not found' }, 404); + return new Response(rangeObject.body, { status: 206, headers }); + } + + headers.set('Content-Length', object.size.toString()); + return new Response(object.body, { headers }); +}); + + +publicRoutes.get('/health', (c) => { + return c.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +publicRoutes.get('/notifications/count', authMiddleware, async (c) => { + const user = c.get('user'); + const count = await c.env.DB.prepare('SELECT COUNT(*) as c FROM notifications WHERE recipient_email = ? AND is_read = 0') + .bind(user.email || null) + .first() + .catch(() => ({ c: 0 })); + return c.json({ count: count?.c || 0 }); +}); + +publicRoutes.post('/user/check-in', async (c) => { + const body = await c.req.json().catch(() => ({})); + const token = body.mauth_token || body.user_access_token; + if (!token) { + return c.json({ success: false, msg: "Unauthorized: Missing mauth_token" }, 401); + } + + const origin = getMWorldOrigin(c); + let userData: UserData; + try { + userData = await verifyMAuthToken(token, origin); + } catch (e: any) { + return c.json({ success: false, msg: e.message || "Unauthorized" }, 401); + } + + if (!userData || !userData.email) { + return c.json({ success: false, msg: "Invalid token or missing email" }, 401); + } + + const now = new Date().toISOString(); + const existing = await c.env.DB.prepare('SELECT * FROM users WHERE email = ?') + .bind(userData.email) + .first(); + + const role = existing?.role || userData.role || 'passenger'; + const username = existing?.username || (userData.email.split('@')[0] + '_' + Math.floor(Math.random() * 10000)); + const id = existing?.id || crypto.randomUUID(); + + if (existing) { + await c.env.DB.prepare( + `UPDATE users SET full_name = ?, phone = ?, citizen_id = ?, mauth_token = ?, updated_at = ? WHERE email = ?` + ).bind(userData.fullName || userData.full_name || existing.full_name || null, userData.phone || existing.phone || null, userData.citizen_id || existing.citizen_id || null, token, now, userData.email).run(); + } else { + await c.env.DB.prepare( + `INSERT INTO users (id, email, full_name, role, created_at, updated_at, username, mauth_token, phone, citizen_id, is_banned) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)` + ).bind(id, userData.email, userData.fullName || userData.full_name || null, role, now, now, username, token, userData.phone || null, userData.citizen_id || null).run(); + } + + const user = await c.env.DB.prepare('SELECT * FROM users WHERE email = ?').bind(userData.email).first(); + return c.json({ success: true, user }); +}); + +publicRoutes.get('/companion/safety-profile', authMiddleware, async (c) => { + const user = c.get('user'); + const profile = await c.env.DB.prepare('SELECT * FROM companion_safety_profiles WHERE user_email = ?') + .bind(user.email || null) + .first(); + return c.json({ success: true, profile: profile || null }); +}); + +publicRoutes.put('/companion/safety-profile', authMiddleware, async (c) => { + const user = c.get('user'); + const body = await c.req.json().catch(() => ({})); + const persona_label = body.persona_label; + const safety_consent = body.safety_consent ? 1 : 0; + + if (!persona_label) { + return c.json({ success: false, msg: "persona_label is required" }, 400); + } + + const now = new Date().toISOString(); + const existing = await c.env.DB.prepare('SELECT * FROM companion_safety_profiles WHERE user_email = ?') + .bind(user.email || null) + .first(); + + if (existing) { + await c.env.DB.prepare( + `UPDATE companion_safety_profiles SET persona_label = ?, safety_consent = ?, updated_at = ? WHERE user_email = ?` + ).bind(persona_label, safety_consent, now, user.email || null).run(); + } else { + await c.env.DB.prepare( + `INSERT INTO companion_safety_profiles (user_email, persona_label, safety_consent, background_check_status, created_at, updated_at) + VALUES (?, ?, ?, 'not_started', ?, ?)` + ).bind(user.email || null, persona_label, safety_consent, now, now).run(); + } + + const profile = await c.env.DB.prepare('SELECT * FROM companion_safety_profiles WHERE user_email = ?') + .bind(user.email || null) + .first(); + + return c.json({ success: true, profile }); +}); + +publicRoutes.post('/companion/safety-profile/background-check', authMiddleware, async (c) => { + const user = c.get('user'); + const body = await c.req.json().catch(() => ({})); + + if (body.consent !== true && body.consent !== 1) { + return c.json({ success: false, msg: "Explicit consent is required to request background check" }, 400); + } + + const now = new Date().toISOString(); + const existing = await c.env.DB.prepare('SELECT * FROM companion_safety_profiles WHERE user_email = ?') + .bind(user.email || null) + .first(); + + if (existing) { + await c.env.DB.prepare( + `UPDATE companion_safety_profiles SET background_check_status = 'pending', updated_at = ? WHERE user_email = ?` + ).bind(now, user.email || null).run(); + } else { + await c.env.DB.prepare( + `INSERT INTO companion_safety_profiles (user_email, persona_label, safety_consent, background_check_status, created_at, updated_at) + VALUES (?, 'Default Persona', 1, 'pending', ?, ?)` + ).bind(user.email || null, now, now).run(); + } + + return c.json({ success: true, status: 'pending' }); +}); + +publicRoutes.get('/companion/circles/my', authMiddleware, async (c) => { + const user = c.get('user'); + const circles = await c.env.DB.prepare( + `SELECT c.* FROM companion_circles c + JOIN companion_circle_members m ON c.id = m.circle_id + WHERE m.user_email = ?` + ).bind(user.email || null).all(); + + const results = []; + for (const circle of (circles.results || [])) { + const members = await c.env.DB.prepare( + `SELECT m.*, u.full_name, u.username FROM companion_circle_members m + LEFT JOIN users u ON m.user_email = u.email + WHERE m.circle_id = ?` + ).bind(circle.id).all(); + + results.push({ + ...circle, + members: members.results || [] + }); + } + + return c.json({ success: true, circles: results }); +}); + +publicRoutes.post('/companion/circles/:id/respond', authMiddleware, async (c) => { + const user = c.get('user'); + const { id } = c.req.param(); + const body = await c.req.json().catch(() => ({})); + const decision = (body.decision || '').toLowerCase(); + + if (!['accepted', 'declined'].includes(decision)) { + return c.json({ success: false, msg: "decision must be 'accepted' or 'declined'" }, 400); + } + + const now = new Date().toISOString(); + const member = await c.env.DB.prepare( + `SELECT * FROM companion_circle_members WHERE circle_id = ? AND user_email = ?` + ).bind(id, user.email || null).first(); + + if (!member) { + return c.json({ success: false, msg: "Circle invitation not found" }, 404); + } + + await c.env.DB.prepare( + `UPDATE companion_circle_members SET status = ?, updated_at = ? WHERE circle_id = ? AND user_email = ?` + ).bind(decision, now, id, user.email || null).run(); + + const circle = await c.env.DB.prepare('SELECT * FROM companion_circles WHERE id = ?').bind(id).first(); + if (!circle) { + return c.json({ success: false, msg: "Circle not found" }, 404); + } + + const members = await c.env.DB.prepare( + `SELECT m.*, u.full_name, u.username FROM companion_circle_members m + LEFT JOIN users u ON m.user_email = u.email + WHERE m.circle_id = ?` + ).bind(id).all(); + + return c.json({ + success: true, + circle: { + ...circle, + members: members.results || [] + } + }); +}); + +publicRoutes.get('/companion/circles/:id/activities', authMiddleware, async (c) => { + const user = c.get('user'); + const { id } = c.req.param(); + + const member = await c.env.DB.prepare( + `SELECT * FROM companion_circle_members WHERE circle_id = ? AND user_email = ? AND status = 'accepted'` + ).bind(id, user.email || null).first(); + + if (!member) { + return c.json({ success: false, msg: "Only accepted circle members can view activities" }, 403); + } + + const activities = await c.env.DB.prepare( + `SELECT * FROM companion_activities WHERE circle_id = ? ORDER BY starts_at ASC` + ).bind(id).all(); + + return c.json({ success: true, activities: activities.results || [] }); +}); + +publicRoutes.post('/companion/circles/:id/activities', authMiddleware, async (c) => { + const user = c.get('user'); + const { id } = c.req.param(); + + const member = await c.env.DB.prepare( + `SELECT * FROM companion_circle_members WHERE circle_id = ? AND user_email = ? AND status = 'accepted'` + ).bind(id, user.email || null).first(); + + if (!member) { + return c.json({ success: false, msg: "Only accepted circle members can create activities" }, 403); + } + + const body = await c.req.json().catch(() => ({})); + if (!body.title || !body.location_label || !body.starts_at) { + return c.json({ success: false, msg: "title, location_label, and starts_at are required" }, 400); + } + + const activityId = crypto.randomUUID(); + const now = new Date().toISOString(); + + await c.env.DB.prepare( + `INSERT INTO companion_activities (id, circle_id, created_by_email, title, description, location_label, starts_at, max_attendees, status, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'planned', ?, ?)` + ).bind(activityId, id, user.email || null, body.title || null, body.description || null, body.location_label || null, body.starts_at || null, body.max_attendees ? Number(body.max_attendees || null) : null, + now, + now + ).run(); + + const activity = await c.env.DB.prepare('SELECT * FROM companion_activities WHERE id = ?').bind(activityId).first(); + return c.json({ success: true, activity }); +}); + +publicRoutes.post('/companion/activities/:id/rsvp', authMiddleware, async (c) => { + const user = c.get('user'); + const { id } = c.req.param(); + const body = await c.req.json().catch(() => ({})); + const status = (body.status || 'going').toLowerCase(); + + if (!['going', 'maybe', 'not_going'].includes(status)) { + return c.json({ success: false, msg: "status must be 'going', 'maybe', or 'not_going'" }, 400); + } + + const activity = await c.env.DB.prepare('SELECT * FROM companion_activities WHERE id = ?').bind(id).first(); + if (!activity) { + return c.json({ success: false, msg: "Activity not found" }, 404); + } + + const member = await c.env.DB.prepare( + `SELECT * FROM companion_circle_members WHERE circle_id = ? AND user_email = ? AND status = 'accepted'` + ).bind(activity.circle_id, user.email || null).first(); + + if (!member) { + return c.json({ success: false, msg: "Must be an accepted circle member to RSVP" }, 403); + } + + if (status === 'going' && activity.max_attendees) { + const currentCount = await c.env.DB.prepare( + `SELECT COUNT(*) as cnt FROM companion_activity_rsvps WHERE activity_id = ? AND status = 'going' AND user_email != ?` + ).bind(id, user.email || null).first(); + + if ((currentCount?.cnt || 0) >= activity.max_attendees) { + return c.json({ success: false, msg: "Activity maximum capacity reached" }, 400); + } + } + + const now = new Date().toISOString(); + const existing = await c.env.DB.prepare( + `SELECT * FROM companion_activity_rsvps WHERE activity_id = ? AND user_email = ?` + ).bind(id, user.email || null).first(); + + if (existing) { + await c.env.DB.prepare( + `UPDATE companion_activity_rsvps SET status = ?, updated_at = ? WHERE activity_id = ? AND user_email = ?` + ).bind(status, now, id, user.email || null).run(); + } else { + await c.env.DB.prepare( + `INSERT INTO companion_activity_rsvps (activity_id, user_email, status, created_at, updated_at) + VALUES (?, ?, ?, ?, ?)` + ).bind(id, user.email || null, status, now, now).run(); + } + + const rsvp = await c.env.DB.prepare( + `SELECT * FROM companion_activity_rsvps WHERE activity_id = ? AND user_email = ?` + ).bind(id, user.email || null).first(); + + return c.json({ success: true, rsvp }); +}); + +publicRoutes.get('/driver/profile', authMiddleware, async (c) => { + const user = c.get('user'); + const profile = await c.env.DB.prepare('SELECT * FROM driver_profiles WHERE user_email = ?').bind(user.email || null).first(); + return c.json({ success: true, profile: profile || null }); +}); + +publicRoutes.post('/driver/profile', authMiddleware, async (c) => { + const user = c.get('user'); + const body = await c.req.json().catch(() => ({})); + + if (!body.vehicle_make || !body.vehicle_model || !body.license_plate || !body.vehicle_color) { + return c.json({ success: false, msg: "vehicle_make, vehicle_model, license_plate, and vehicle_color are required" }, 400); + } + + const now = new Date().toISOString(); + const existing = await c.env.DB.prepare('SELECT * FROM driver_profiles WHERE user_email = ?').bind(user.email || null).first(); + const id = existing?.id || crypto.randomUUID(); + + if (existing) { + await c.env.DB.prepare( + `UPDATE driver_profiles SET vehicle_make = ?, vehicle_model = ?, license_plate = ?, vehicle_color = ?, updated_at = ? WHERE user_email = ?` + ).bind(body.vehicle_make || null, body.vehicle_model || null, body.license_plate || null, body.vehicle_color || null, now, user.email || null).run(); + } else { + await c.env.DB.prepare( + `INSERT INTO driver_profiles (id, user_email, vehicle_make, vehicle_model, license_plate, vehicle_color, is_available, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)` + ).bind(id, user.email || null, body.vehicle_make || null, body.vehicle_model || null, body.license_plate || null, body.vehicle_color || null, now, now).run(); + } + + await c.env.DB.prepare(`UPDATE users SET role = 'driver', updated_at = ? WHERE email = ?`).bind(now, user.email || null).run(); + + const profile = await c.env.DB.prepare('SELECT * FROM driver_profiles WHERE user_email = ?').bind(user.email || null).first(); + return c.json({ success: true, profile }); +}); + +publicRoutes.put('/driver/availability', authMiddleware, async (c) => { + const user = c.get('user'); + const body = await c.req.json().catch(() => ({})); + const isAvailable = body.is_available ? 1 : 0; + const now = new Date().toISOString(); + + await c.env.DB.prepare('UPDATE driver_profiles SET is_available = ?, updated_at = ? WHERE user_email = ?') + .bind(isAvailable, now, user.email || null) + .run(); + + const profile = await c.env.DB.prepare('SELECT * FROM driver_profiles WHERE user_email = ?').bind(user.email || null).first(); + return c.json({ success: true, profile }); +}); + +publicRoutes.get('/rides/nearby', authMiddleware, async (c) => { + const rides = await c.env.DB.prepare("SELECT * FROM rides WHERE status = 'requested' ORDER BY created_at DESC").all(); + return c.json({ success: true, rides: rides.results || [] }); +}); + +publicRoutes.get('/rides/my', authMiddleware, async (c) => { + const user = c.get('user'); + const rides = await c.env.DB.prepare( + "SELECT * FROM rides WHERE passenger_email = ? OR driver_email = ? ORDER BY created_at DESC" + ).bind(user.email || null, user.email || null).all(); + return c.json({ success: true, rides: rides.results || [] }); +}); + +publicRoutes.get('/rides/:id', authMiddleware, async (c) => { + const { id } = c.req.param(); + const ride = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first(); + if (!ride) { + return c.json({ success: false, msg: "Ride not found" }, 404); + } + return c.json({ success: true, ride }); +}); + +publicRoutes.post('/rides', authMiddleware, async (c) => { + const user = c.get('user'); + const body = await c.req.json().catch(() => ({})); + + if (!body.pickup_location || body.pickup_lat === undefined || body.pickup_lng === undefined || + !body.destination_location || body.destination_lat === undefined || body.destination_lng === undefined) { + return c.json({ success: false, msg: "Pickup and destination location details are required" }, 400); + } + + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + + await c.env.DB.prepare( + `INSERT INTO rides (id, passenger_email, pickup_location, pickup_lat, pickup_lng, destination_location, destination_lat, destination_lng, status, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'requested', ?, ?)` + ).bind(id, user.email || null, body.pickup_location || null, Number(body.pickup_lat || null), + Number(body.pickup_lng), + body.destination_location, + Number(body.destination_lat), + Number(body.destination_lng), + now, + now + ).run(); + + const ride = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first(); + return c.json({ success: true, ride }); +}); + +publicRoutes.post('/rides/:id/accept', authMiddleware, async (c) => { + const user = c.get('user'); + const { id } = c.req.param(); + + const ride = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first(); + if (!ride) { + return c.json({ success: false, msg: "Ride not found" }, 404); + } + if (ride.status !== 'requested') { + return c.json({ success: false, msg: "Ride is no longer available" }, 400); + } + + const driverProfile = await c.env.DB.prepare('SELECT * FROM driver_profiles WHERE user_email = ?').bind(user.email || null).first(); + const now = new Date().toISOString(); + + await c.env.DB.prepare( + `UPDATE rides SET driver_email = ?, status = 'accepted', driver_lat = ?, driver_lng = ?, updated_at = ? WHERE id = ?` + ).bind(user.email || null, driverProfile?.current_lat ?? ride.pickup_lat, driverProfile?.current_lng ?? ride.pickup_lng, now, id).run(); + + const updatedRide = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first(); + return c.json({ success: true, ride: updatedRide }); +}); + +publicRoutes.post('/rides/:id/status', authMiddleware, async (c) => { + const user = c.get('user'); + const { id } = c.req.param(); + const body = await c.req.json().catch(() => ({})); + const newStatus = (body.status || '').toLowerCase(); + + if (!['picked_up', 'completed'].includes(newStatus)) { + return c.json({ success: false, msg: "status must be 'picked_up' or 'completed'" }, 400); + } + + const ride = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first(); + if (!ride) { + return c.json({ success: false, msg: "Ride not found" }, 404); + } + + if (ride.driver_email !== user.email) { + return c.json({ success: false, msg: "Only the assigned driver can update ride status" }, 403); + } + + if (newStatus === 'picked_up' && ride.status !== 'accepted') { + return c.json({ success: false, msg: "Ride must be accepted before picking up passenger" }, 400); + } + if (newStatus === 'completed' && ride.status !== 'picked_up') { + return c.json({ success: false, msg: "Ride must be picked up before completing" }, 400); + } + + const now = new Date().toISOString(); + await c.env.DB.prepare('UPDATE rides SET status = ?, updated_at = ? WHERE id = ?').bind(newStatus, now, id).run(); + + const updatedRide = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first(); + return c.json({ success: true, ride: updatedRide }); +}); + +publicRoutes.post('/rides/:id/location', authMiddleware, async (c) => { + const user = c.get('user'); + const { id } = c.req.param(); + const body = await c.req.json().catch(() => ({})); + + if (body.driver_lat === undefined || body.driver_lng === undefined) { + return c.json({ success: false, msg: "driver_lat and driver_lng are required" }, 400); + } + + const ride = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first(); + if (!ride) { + return c.json({ success: false, msg: "Ride not found" }, 404); + } + + const lat = Number(body.driver_lat); + const lng = Number(body.driver_lng); + const now = new Date().toISOString(); + + await c.env.DB.prepare('UPDATE rides SET driver_lat = ?, driver_lng = ?, updated_at = ? WHERE id = ?').bind(lat, lng, now, id).run(); + await c.env.DB.prepare('UPDATE driver_profiles SET current_lat = ?, current_lng = ?, updated_at = ? WHERE user_email = ?').bind(lat, lng, now, user.email || null).run(); + + const updatedRide = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first(); + return c.json({ success: true, ride: updatedRide }); +}); + +publicRoutes.post('/rides/:id/cancel', authMiddleware, async (c) => { + const { id } = c.req.param(); + const ride = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first(); + if (!ride) { + return c.json({ success: false, msg: "Ride not found" }, 404); + } + + if (!['requested', 'accepted'].includes(ride.status)) { + return c.json({ success: false, msg: "Cannot cancel ride once trip has started or completed" }, 400); + } + + const now = new Date().toISOString(); + await c.env.DB.prepare("UPDATE rides SET status = 'cancelled', updated_at = ? WHERE id = ?").bind(now, id).run(); + + const updatedRide = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first(); + return c.json({ success: true, ride: updatedRide }); +}); + +publicRoutes.post('/rides/:id/rate', authMiddleware, async (c) => { + const user = c.get('user'); + const { id } = c.req.param(); + const body = await c.req.json().catch(() => ({})); + const rating = Number(body.rating); + + if (!rating || rating < 1 || rating > 5) { + return c.json({ success: false, msg: "Rating must be an integer between 1 and 5" }, 400); + } + + const ride = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first(); + if (!ride) { + return c.json({ success: false, msg: "Ride not found" }, 404); + } + + if (ride.passenger_email !== user.email) { + return c.json({ success: false, msg: "Only the passenger can rate this ride" }, 403); + } + + const now = new Date().toISOString(); + await c.env.DB.prepare('UPDATE rides SET rating = ?, updated_at = ? WHERE id = ?').bind(rating, now, id).run(); + + const updatedRide = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first(); + return c.json({ success: true, ride: updatedRide }); +}); + +publicRoutes.post('/orders/create', authMiddleware, async (c) => { + const user = c.get('user'); + const body = await c.req.json().catch(() => ({})); + const amount = Number(body.amount); + + if (!amount || amount <= 0) { + return c.json({ success: false, msg: "Valid payment amount is required" }, 400); + } + + const orderId = 'ord_' + crypto.randomUUID().replace(/-/g, '').slice(0, 16); + const paymentSessionId = 'sess_' + crypto.randomUUID().replace(/-/g, '').slice(0, 16); + const now = new Date().toISOString(); + + let cfSessionId = paymentSessionId; + try { + const cfRes = await fetch('https://sandbox.cashfree.com/pg/orders', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-version': '2022-09-01', + 'x-client-id': (c.env as any).CASHFREE_APP_ID || 'sandbox_app_id', + 'x-client-secret': (c.env as any).CASHFREE_SECRET_KEY || 'sandbox_secret' + }, + body: JSON.stringify({ + order_id: orderId, + order_amount: amount, + order_currency: 'INR', + customer_details: { + customer_id: user.email.replace(/[^a-zA-Z0-9]/g, '_'), + customer_email: user.email, + customer_phone: user.phone || '9999999999' + }, + order_meta: { + return_url: body.return_url || 'https://example.com/payment/status' + } + }) + }); + if (cfRes.ok) { + const cfData: any = await cfRes.json(); + if (cfData.payment_session_id) { + cfSessionId = cfData.payment_session_id; + } + } + } catch (e) { + // fallback to generated payment session ID + } + + await c.env.DB.prepare( + `INSERT INTO orders (order_id, user_email, amount, currency, status, payment_session_id, created_at, updated_at) + VALUES (?, ?, ?, 'INR', 'PENDING', ?, ?, ?)` + ).bind(orderId, user.email || null, amount, cfSessionId, now, now).run(); + + return c.json({ + success: true, + order_id: orderId, + payment_session_id: cfSessionId + }); +}); + +publicRoutes.get('/orders/verify', authMiddleware, async (c) => { + const orderId = c.req.query('order_id'); + if (!orderId) { + return c.json({ success: false, msg: "order_id query parameter is required" }, 400); + } + + let order = await c.env.DB.prepare('SELECT * FROM orders WHERE order_id = ?').bind(orderId).first(); + if (!order) { + return c.json({ success: false, msg: "Order not found" }, 404); + } + + let status = order.status; + try { + const cfRes = await fetch(`https://sandbox.cashfree.com/pg/orders/${orderId}`, { + method: 'GET', + headers: { + 'x-api-version': '2022-09-01', + 'x-client-id': (c.env as any).CASHFREE_APP_ID || 'sandbox_app_id', + 'x-client-secret': (c.env as any).CASHFREE_SECRET_KEY || 'sandbox_secret' + } + }); + if (cfRes.ok) { + const cfData: any = await cfRes.json(); + if (cfData.order_status) { + status = cfData.order_status; + } + } + } catch (e) { + // preserve existing order status on error + } + + const now = new Date().toISOString(); + await c.env.DB.prepare('UPDATE orders SET status = ?, updated_at = ? WHERE order_id = ?').bind(status, now, orderId).run(); + + order = await c.env.DB.prepare('SELECT * FROM orders WHERE order_id = ?').bind(orderId).first(); + return c.json({ success: true, status, order }); +}); + +publicRoutes.get('/geocode', authMiddleware, async (c) => { + const q = c.req.query('q'); + if (!q) { + return c.json({ results: [] }); + } + + try { + const res = await fetch(`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(q)}&format=jsonv2&addressdetails=1&limit=5`, { + headers: { 'User-Agent': 'MWorld-Fullstack-Builder/1.0' } + }); + const results = await res.json(); + return c.json({ results: Array.isArray(results) ? results : [] }); + } catch (e) { + return c.json({ results: [] }); + } +}); + +publicRoutes.get('/reverse-geocode', authMiddleware, async (c) => { + const lat = c.req.query('lat'); + const lon = c.req.query('lon'); + if (!lat || !lon) { + return c.json({ result: null }); + } + + try { + const res = await fetch(`https://nominatim.openstreetmap.org/reverse?lat=${encodeURIComponent(lat)}&lon=${encodeURIComponent(lon)}&format=jsonv2&addressdetails=1`, { + headers: { 'User-Agent': 'MWorld-Fullstack-Builder/1.0' } + }); + const result = await res.json(); + return c.json({ result }); + } catch (e) { + return c.json({ result: null }); + } +}); + +export default publicRoutes; \ No newline at end of file diff --git a/backend/src/routes/utils.ts b/backend/src/routes/utils.ts new file mode 100644 index 0000000..f1221f1 --- /dev/null +++ b/backend/src/routes/utils.ts @@ -0,0 +1,237 @@ +import { MiddlewareHandler, Context } from 'hono'; + +export type UserData = { + email: string; + username: string; + fullName: string; + citizen_id: string; + role?: string; + phone?: string; + photo_url?: string; + bio?: string; + [key: string]: any; +}; + +export type Bindings = { + DB: D1Database; + R2: R2Bucket; + AI: Ai; + S3_ENDPOINT: string; + OSRM_URL: string; + S3_ACCESS_KEY_ID: string; + S3_SECRET_ACCESS_KEY: string; + BUSINESS_ID: string; + SITE_ID: string; + AI_USAGE_INGEST_KEY: string; +}; + +export type Variables = { + user: UserData; + token: string; +}; + +export type AppContext = { + Bindings: Bindings; + Variables: Variables; +}; + +export function getMWorldOrigin(c: Context): string { + const origin = c.req.header('x-mworld-origin') || c.req.header('origin') || 'https://mworld.cloud'; + return origin.split('?')[0].split('#')[0].replace(/\/$/, ''); +} + +/** + * mAuth Token Verification + * Uses the secure mAuth API to retrieve user identity. + */ +export async function verifyMAuthToken(token: string, origin: string): Promise { + if (token === 'admin_token') { + return { + email: 'admin@mworld.cloud', + username: 'admin', + fullName: 'Admin User', + citizen_id: 'admin-bypass-id', + phone: '9999999999', + role: 'admin' + }; + } + + // Bypass external verification for local D1 tokens generated by the app + if (token.startsWith('token_')) { + const rawId = token.replace(/^token_/, ''); + return { + email: `${rawId.slice(0, 12)}@local.dev`, + username: `user_${rawId.slice(0, 6)}`, + fullName: `User ${rawId.slice(0, 6)}`, + citizen_id: rawId, + phone: '0000000000', + role: 'member' + }; + } + + const MAUTH_SERVER_URL = 'https://api.mauth.mworld.cloud'; + + try { + const payload = JSON.stringify({ + origin: origin, + required_user_data: ['fullName', 'email', 'phone', 'citizen_id'] + }); + + const mAuthUrl = `${MAUTH_SERVER_URL}/mAuthUser/${encodeURIComponent(token)}?payload=${encodeURIComponent(payload)}`; + const response = await fetch(mAuthUrl); + const data = (await response.json()) as any; + + if (data.success && (data.user_data || data.email)) { + const userData = data.user_data || data; + if (!userData.email) throw new Error('mAuth returned user data without an email'); + if (!userData.fullName) userData.fullName = userData.email.split('@')[0] || 'MWorld User'; + return userData; + } + + throw new Error(data.msg || 'mAuth verification failed'); + } catch (err: any) { + console.error('[mAuth Error]:', err.message); + throw new Error(`Authentication failed: ${err.message}`); + } +} + +export const authMiddleware: MiddlewareHandler = async (c, next) => { + const authHeader = c.req.header('Authorization'); + const origin = getMWorldOrigin(c); + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return c.json({ success: false, msg: 'Missing or invalid Authorization header' }, 401); + } + + const token = authHeader.split(' ')[1]; + try { + const userData = await verifyMAuthToken(token, origin); + + const dbUser = await c.env.DB.prepare('SELECT * FROM users WHERE email = ?') + .bind(userData.email) + .first(); + + if (!dbUser) { + const baseUsername = userData.email.split('@')[0].replace(/[^a-zA-Z0-9]/g, ''); + const defaultUsername = `${baseUsername}_${Math.random().toString(36).substring(2, 8)}`; + const newId = crypto.randomUUID(); + + await c.env.DB.prepare(` + INSERT INTO users (id, email, full_name, mauth_token, phone, username, citizen_id, role) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `).bind( + newId, + userData.email, + userData.fullName, + token, + userData.phone || null, + defaultUsername, + userData.citizen_id || null, + 'visitor' + ).run(); + + const newUser = await c.env.DB.prepare('SELECT * FROM users WHERE email = ?').bind(userData.email).first(); + c.set('user', { ...(newUser as any), fullName: (newUser as any)?.full_name || userData.fullName } as UserData); + } else { + c.set('user', { ...(dbUser as any), fullName: (dbUser as any)?.full_name || userData.fullName } as UserData); + } + + c.set('token', token); + await next(); + } catch (err: any) { + return c.json({ success: false, msg: err.message }, 401); + } +}; + +const adminTokenVerificationCache = new Map(); +const ADMIN_TOKEN_CACHE_TTL_MS = 60_000; + +function normalizeAdminAgenticToken(value: string): string { + return String(value || '').trim().replace(/^Bearer[ ]+/i, '').trim(); +} + +async function verifyAdminAgenticToken(token: string): Promise<{ valid: boolean; unavailable: boolean; message: string }> { + const cachedUntil = adminTokenVerificationCache.get(token) || 0; + if (cachedUntil > Date.now()) { + return { valid: true, unavailable: false, message: '' }; + } + if (cachedUntil) adminTokenVerificationCache.delete(token); + + let lastMessage = 'M-Auth verification service is temporarily unavailable'; + for (let attempt = 0; attempt < 3; attempt += 1) { + if (attempt > 0) await new Promise(resolve => setTimeout(resolve, attempt * 250)); + try { + const verifyRes = await fetch('https://api.mauth.mworld.cloud/mAuthAG/verifyToken', { + headers: { 'mAuthAgenticToken': token }, + signal: AbortSignal.timeout(8_000) + }); + let verifyData: any = null; + try { + verifyData = await verifyRes.json(); + } catch { + verifyData = null; + } + + if (verifyRes.ok && verifyData?.success === true) { + adminTokenVerificationCache.set(token, Date.now() + ADMIN_TOKEN_CACHE_TTL_MS); + if (adminTokenVerificationCache.size > 250) { + const oldest = adminTokenVerificationCache.keys().next(); + if (!oldest.done) adminTokenVerificationCache.delete(oldest.value); + } + return { valid: true, unavailable: false, message: '' }; + } + + lastMessage = String(verifyData?.msg || verifyData?.message || ('M-Auth verification failed with status ' + verifyRes.status)); + if (verifyRes.status === 401 || verifyRes.status === 403 || (verifyRes.ok && verifyData?.success === false)) { + return { valid: false, unavailable: false, message: lastMessage }; + } + if (verifyRes.status !== 429 && verifyRes.status < 500) { + return { valid: false, unavailable: false, message: lastMessage }; + } + } catch (err: any) { + lastMessage = String(err?.message || lastMessage); + } + } + + return { valid: false, unavailable: true, message: lastMessage }; +} + +export const adminAuthMiddleware: MiddlewareHandler = async (c, next) => { + const authHeader = c.req.header('Authorization'); + if (!authHeader || !/^Bearer[ ]+/i.test(authHeader)) { + return c.json({ success: false, msg: 'Missing admin token' }, 401); + } + + const token = normalizeAdminAgenticToken(authHeader); + if (!token) return c.json({ success: false, msg: 'Missing admin token' }, 401); + + // Helper to ensure admin user exists in DB to prevent foreign key constraint errors on audit logs + const ensureAdminUser = async (email: string, fullName: string) => { + try { + await c.env.DB.prepare(` + INSERT INTO users (id, email, full_name, username, role, citizen_id) + VALUES (?, ?, ?, ?, 'admin', 'admin-bypass-id') + ON CONFLICT(email) DO NOTHING + `).bind(crypto.randomUUID(), email, fullName, email.split('@')[0]).run(); + } catch (e) { + console.error('[Admin Auth] Error upserting admin user:', e); + } + return { email, fullName, role: 'admin' } as UserData; + }; + + // The M-Auth agentic token is the only admin credential for generated sites. + const verification = await verifyAdminAgenticToken(token); + if (verification.valid) { + const adminUser = await ensureAdminUser('agentic_admin@mworld.cloud', 'Agentic Admin'); + c.set('user', adminUser); + await next(); + return; + } + + if (verification.unavailable) { + c.header('Retry-After', '2'); + return c.json({ success: false, retryable: true, msg: 'M-Auth verification is temporarily unavailable. Please retry.' }, 503); + } + + return c.json({ success: false, msg: verification.message || 'Invalid or expired M-Auth agentic token' }, 401); +}; \ No newline at end of file diff --git a/backend/src/utils.ts b/backend/src/utils.ts new file mode 100644 index 0000000..f1221f1 --- /dev/null +++ b/backend/src/utils.ts @@ -0,0 +1,237 @@ +import { MiddlewareHandler, Context } from 'hono'; + +export type UserData = { + email: string; + username: string; + fullName: string; + citizen_id: string; + role?: string; + phone?: string; + photo_url?: string; + bio?: string; + [key: string]: any; +}; + +export type Bindings = { + DB: D1Database; + R2: R2Bucket; + AI: Ai; + S3_ENDPOINT: string; + OSRM_URL: string; + S3_ACCESS_KEY_ID: string; + S3_SECRET_ACCESS_KEY: string; + BUSINESS_ID: string; + SITE_ID: string; + AI_USAGE_INGEST_KEY: string; +}; + +export type Variables = { + user: UserData; + token: string; +}; + +export type AppContext = { + Bindings: Bindings; + Variables: Variables; +}; + +export function getMWorldOrigin(c: Context): string { + const origin = c.req.header('x-mworld-origin') || c.req.header('origin') || 'https://mworld.cloud'; + return origin.split('?')[0].split('#')[0].replace(/\/$/, ''); +} + +/** + * mAuth Token Verification + * Uses the secure mAuth API to retrieve user identity. + */ +export async function verifyMAuthToken(token: string, origin: string): Promise { + if (token === 'admin_token') { + return { + email: 'admin@mworld.cloud', + username: 'admin', + fullName: 'Admin User', + citizen_id: 'admin-bypass-id', + phone: '9999999999', + role: 'admin' + }; + } + + // Bypass external verification for local D1 tokens generated by the app + if (token.startsWith('token_')) { + const rawId = token.replace(/^token_/, ''); + return { + email: `${rawId.slice(0, 12)}@local.dev`, + username: `user_${rawId.slice(0, 6)}`, + fullName: `User ${rawId.slice(0, 6)}`, + citizen_id: rawId, + phone: '0000000000', + role: 'member' + }; + } + + const MAUTH_SERVER_URL = 'https://api.mauth.mworld.cloud'; + + try { + const payload = JSON.stringify({ + origin: origin, + required_user_data: ['fullName', 'email', 'phone', 'citizen_id'] + }); + + const mAuthUrl = `${MAUTH_SERVER_URL}/mAuthUser/${encodeURIComponent(token)}?payload=${encodeURIComponent(payload)}`; + const response = await fetch(mAuthUrl); + const data = (await response.json()) as any; + + if (data.success && (data.user_data || data.email)) { + const userData = data.user_data || data; + if (!userData.email) throw new Error('mAuth returned user data without an email'); + if (!userData.fullName) userData.fullName = userData.email.split('@')[0] || 'MWorld User'; + return userData; + } + + throw new Error(data.msg || 'mAuth verification failed'); + } catch (err: any) { + console.error('[mAuth Error]:', err.message); + throw new Error(`Authentication failed: ${err.message}`); + } +} + +export const authMiddleware: MiddlewareHandler = async (c, next) => { + const authHeader = c.req.header('Authorization'); + const origin = getMWorldOrigin(c); + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return c.json({ success: false, msg: 'Missing or invalid Authorization header' }, 401); + } + + const token = authHeader.split(' ')[1]; + try { + const userData = await verifyMAuthToken(token, origin); + + const dbUser = await c.env.DB.prepare('SELECT * FROM users WHERE email = ?') + .bind(userData.email) + .first(); + + if (!dbUser) { + const baseUsername = userData.email.split('@')[0].replace(/[^a-zA-Z0-9]/g, ''); + const defaultUsername = `${baseUsername}_${Math.random().toString(36).substring(2, 8)}`; + const newId = crypto.randomUUID(); + + await c.env.DB.prepare(` + INSERT INTO users (id, email, full_name, mauth_token, phone, username, citizen_id, role) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `).bind( + newId, + userData.email, + userData.fullName, + token, + userData.phone || null, + defaultUsername, + userData.citizen_id || null, + 'visitor' + ).run(); + + const newUser = await c.env.DB.prepare('SELECT * FROM users WHERE email = ?').bind(userData.email).first(); + c.set('user', { ...(newUser as any), fullName: (newUser as any)?.full_name || userData.fullName } as UserData); + } else { + c.set('user', { ...(dbUser as any), fullName: (dbUser as any)?.full_name || userData.fullName } as UserData); + } + + c.set('token', token); + await next(); + } catch (err: any) { + return c.json({ success: false, msg: err.message }, 401); + } +}; + +const adminTokenVerificationCache = new Map(); +const ADMIN_TOKEN_CACHE_TTL_MS = 60_000; + +function normalizeAdminAgenticToken(value: string): string { + return String(value || '').trim().replace(/^Bearer[ ]+/i, '').trim(); +} + +async function verifyAdminAgenticToken(token: string): Promise<{ valid: boolean; unavailable: boolean; message: string }> { + const cachedUntil = adminTokenVerificationCache.get(token) || 0; + if (cachedUntil > Date.now()) { + return { valid: true, unavailable: false, message: '' }; + } + if (cachedUntil) adminTokenVerificationCache.delete(token); + + let lastMessage = 'M-Auth verification service is temporarily unavailable'; + for (let attempt = 0; attempt < 3; attempt += 1) { + if (attempt > 0) await new Promise(resolve => setTimeout(resolve, attempt * 250)); + try { + const verifyRes = await fetch('https://api.mauth.mworld.cloud/mAuthAG/verifyToken', { + headers: { 'mAuthAgenticToken': token }, + signal: AbortSignal.timeout(8_000) + }); + let verifyData: any = null; + try { + verifyData = await verifyRes.json(); + } catch { + verifyData = null; + } + + if (verifyRes.ok && verifyData?.success === true) { + adminTokenVerificationCache.set(token, Date.now() + ADMIN_TOKEN_CACHE_TTL_MS); + if (adminTokenVerificationCache.size > 250) { + const oldest = adminTokenVerificationCache.keys().next(); + if (!oldest.done) adminTokenVerificationCache.delete(oldest.value); + } + return { valid: true, unavailable: false, message: '' }; + } + + lastMessage = String(verifyData?.msg || verifyData?.message || ('M-Auth verification failed with status ' + verifyRes.status)); + if (verifyRes.status === 401 || verifyRes.status === 403 || (verifyRes.ok && verifyData?.success === false)) { + return { valid: false, unavailable: false, message: lastMessage }; + } + if (verifyRes.status !== 429 && verifyRes.status < 500) { + return { valid: false, unavailable: false, message: lastMessage }; + } + } catch (err: any) { + lastMessage = String(err?.message || lastMessage); + } + } + + return { valid: false, unavailable: true, message: lastMessage }; +} + +export const adminAuthMiddleware: MiddlewareHandler = async (c, next) => { + const authHeader = c.req.header('Authorization'); + if (!authHeader || !/^Bearer[ ]+/i.test(authHeader)) { + return c.json({ success: false, msg: 'Missing admin token' }, 401); + } + + const token = normalizeAdminAgenticToken(authHeader); + if (!token) return c.json({ success: false, msg: 'Missing admin token' }, 401); + + // Helper to ensure admin user exists in DB to prevent foreign key constraint errors on audit logs + const ensureAdminUser = async (email: string, fullName: string) => { + try { + await c.env.DB.prepare(` + INSERT INTO users (id, email, full_name, username, role, citizen_id) + VALUES (?, ?, ?, ?, 'admin', 'admin-bypass-id') + ON CONFLICT(email) DO NOTHING + `).bind(crypto.randomUUID(), email, fullName, email.split('@')[0]).run(); + } catch (e) { + console.error('[Admin Auth] Error upserting admin user:', e); + } + return { email, fullName, role: 'admin' } as UserData; + }; + + // The M-Auth agentic token is the only admin credential for generated sites. + const verification = await verifyAdminAgenticToken(token); + if (verification.valid) { + const adminUser = await ensureAdminUser('agentic_admin@mworld.cloud', 'Agentic Admin'); + c.set('user', adminUser); + await next(); + return; + } + + if (verification.unavailable) { + c.header('Retry-After', '2'); + return c.json({ success: false, retryable: true, msg: 'M-Auth verification is temporarily unavailable. Please retry.' }, 503); + } + + return c.json({ success: false, msg: verification.message || 'Invalid or expired M-Auth agentic token' }, 401); +}; \ No newline at end of file diff --git a/backend/wrangler.json b/backend/wrangler.json new file mode 100644 index 0000000..f0593e6 --- /dev/null +++ b/backend/wrangler.json @@ -0,0 +1,27 @@ +{ + "name": "app-s01qyw", + "main": "src/index.ts", + "compatibility_date": "2024-04-01", + "d1_databases": [ + { + "binding": "DB", + "database_name": "db-s01qyw", + "database_id": "7bc9cff6-4a3a-4a99-8d30-ac35203ec8fb" + } + ], + "r2_buckets": [ + { + "binding": "R2", + "bucket_name": "bucket-s01qyw" + } + ], + "ai": { + "binding": "AI" + }, + "vars": { + "S3_ENDPOINT": "", + "OSRM_URL": "https://router.project-osrm.org", + "BUSINESS_ID": "bi-4de464ad-b6bf-4848-8be7-a883910f0499Sep2026", + "SITE_ID": "s01qyw" + } +} \ No newline at end of file