diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..cdd16c0 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,4 @@ +node_modules +.wrangler +dist +.env diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..f301613 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,1652 @@ +{ + "name": "app-xd1uom", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "app-xd1uom", + "dependencies": { + "hono": "latest" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.0.0", + "typescript": "^5.0.0", + "wrangler": "^3.109.2" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz", + "integrity": "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "mime": "^3.0.0" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.0.2.tgz", + "integrity": "sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.14", + "workerd": "^1.20250124.0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250718.0.tgz", + "integrity": "sha512-FHf4t7zbVN8yyXgQ/r/GqLPaYZSGUVzeR7RnL28Mwj2djyw2ZergvytVc7fdGcczl6PQh+VKGfZCfUqpJlbi9g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250718.0.tgz", + "integrity": "sha512-fUiyUJYyqqp4NqJ0YgGtp4WJh/II/YZsUnEb6vVy5Oeas8lUOxnN+ZOJ8N/6/5LQCVAtYCChRiIrBbfhTn5Z8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250718.0.tgz", + "integrity": "sha512-5+eb3rtJMiEwp08Kryqzzu8d1rUcK+gdE442auo5eniMpT170Dz0QxBrqkg2Z48SFUPYbj+6uknuA5tzdRSUSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250718.0.tgz", + "integrity": "sha512-Aa2M/DVBEBQDdATMbn217zCSFKE+ud/teS+fFS+OQqKABLn0azO2qq6ANAHYOIE6Q3Sq4CxDIQr8lGdaJHwUog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250718.0.tgz", + "integrity": "sha512-dY16RXKffmugnc67LTbyjdDHZn5NoTF1yHEf2fN4+OaOnoGSp3N1x77QubTDwqZ9zECWxgQfDLjddcH8dWeFhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20260702.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260702.1.tgz", + "integrity": "sha512-mOhf5TUEB1m2vPrxtqoIGfz0fUC9xyxRDx5gWHy5s+OCo6dcV+g7wI1R7gYCMFohhqF/2y2xeKVwMwCJjfn/WA==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild-plugins/node-globals-polyfill": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz", + "integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/@esbuild-plugins/node-modules-polyfill": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz", + "integrity": "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==", + "dev": true, + "license": "ISC", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "rollup-plugin-node-polyfills": "^0.2.1" + }, + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/as-table": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", + "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "printable-characters": "^1.0.42" + } + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", + "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", + "dev": true, + "license": "MIT" + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/exit-hook": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exsolve": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.0.tgz", + "integrity": "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==", + "dev": true, + "license": "MIT" + }, + "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/get-source": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", + "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "data-uri-to-buffer": "^2.0.0", + "source-map": "^0.6.1" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/hono": { + "version": "4.12.30", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.30.tgz", + "integrity": "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/miniflare": { + "version": "3.20250718.3", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20250718.3.tgz", + "integrity": "sha512-JuPrDJhwLrNLEJiNLWO7ZzJrv/Vv9kZuwMYCfv0LskQDM6Eonw4OvywO3CH/wCGjgHzha/qyjUh8JQ068TjDgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "acorn": "8.14.0", + "acorn-walk": "8.3.2", + "exit-hook": "2.2.1", + "glob-to-regexp": "0.4.1", + "stoppable": "1.1.0", + "undici": "^5.28.5", + "workerd": "1.20250718.0", + "ws": "8.18.0", + "youch": "3.3.4", + "zod": "3.22.3" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/printable-characters": { + "version": "1.0.42", + "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/rollup-plugin-inject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz", + "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^0.6.1", + "magic-string": "^0.25.3", + "rollup-pluginutils": "^2.8.1" + } + }, + "node_modules/rollup-plugin-node-polyfills": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz", + "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rollup-plugin-inject": "^3.0.0" + } + }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true, + "license": "MIT" + }, + "node_modules/stacktracey": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.2.0.tgz", + "integrity": "sha512-ETyQEz+CzXiLjEbyJqpbp+/T79RQD/6wqFucRBIlVNZfYq2Ay7wbretD4cxpbymZlaPWx58aIhPEY1Cr8DlVvg==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "as-table": "^1.0.36", + "get-source": "^2.0.12" + } + }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.14", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.14.tgz", + "integrity": "sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "exsolve": "^1.0.1", + "ohash": "^2.0.10", + "pathe": "^2.0.3", + "ufo": "^1.5.4" + } + }, + "node_modules/workerd": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250718.0.tgz", + "integrity": "sha512-kqkIJP/eOfDlUyBzU7joBg+tl8aB25gEAGqDap+nFWb+WHhnooxjGHgxPBy3ipw2hnShPFNOQt5lFRxbwALirg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20250718.0", + "@cloudflare/workerd-darwin-arm64": "1.20250718.0", + "@cloudflare/workerd-linux-64": "1.20250718.0", + "@cloudflare/workerd-linux-arm64": "1.20250718.0", + "@cloudflare/workerd-windows-64": "1.20250718.0" + } + }, + "node_modules/wrangler": { + "version": "3.114.17", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.114.17.tgz", + "integrity": "sha512-tAvf7ly+tB+zwwrmjsCyJ2pJnnc7SZhbnNwXbH+OIdVas3zTSmjcZOjmLKcGGptssAA3RyTKhcF9BvKZzMUycA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.3.4", + "@cloudflare/unenv-preset": "2.0.2", + "@esbuild-plugins/node-globals-polyfill": "0.2.3", + "@esbuild-plugins/node-modules-polyfill": "0.2.2", + "blake3-wasm": "2.1.5", + "esbuild": "0.17.19", + "miniflare": "3.20250718.3", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.14", + "workerd": "1.20250718.0" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=16.17.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2", + "sharp": "^0.33.5" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20250408.0" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "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": "3.3.4", + "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", + "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie": "^0.7.1", + "mustache": "^4.2.0", + "stacktracey": "^2.1.8" + } + }, + "node_modules/zod": { + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..245c3b8 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,15 @@ +{ + "name": "app-xd1uom", + "type": "module", + "scripts": { + "dev": "wrangler dev" + }, + "dependencies": { + "hono": "latest" + }, + "devDependencies": { + "wrangler": "^3.109.2", + "typescript": "^5.0.0", + "@cloudflare/workers-types": "^4.0.0" + } +} \ No newline at end of file diff --git a/backend/src/index.ts b/backend/src/index.ts new file mode 100644 index 0000000..59e9dad --- /dev/null +++ b/backend/src/index.ts @@ -0,0 +1,111 @@ +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { AppContext } from './utils'; +import publicRoutes from './routes/public'; +import adminRoutes from './routes/admin'; + +const app = new Hono(); + +// CORS Middleware +app.use( + '*', + cors({ + origin: '*', // In production, restrict this to your frontend's origin + allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowHeaders: ['Content-Type', 'Authorization', 'X-Anonymous-User-ID', 'x-mworld-origin'], + }) +); + +// Health check +app.get('/', (c) => c.text('API is running')); + +// Mount API routes +app.route('/api', publicRoutes); +app.route('/api/admin', adminRoutes); + +// 404 Handler +app.notFound((c) => { + return c.json({ success: false, error: 'Not found' }, 404); +}); + +// Error Handler +app.onError((err, c) => { + console.error('An error occurred:', err); + return c.json({ success: false, error: 'An internal error occurred' }, 500); +}); + +// Scheduled Task for dissolving posts +const scheduledTask = async (env: AppContext['Bindings']) => { + console.log('Running scheduled task: Dissolving expired posts...'); + const { DB } = env; + + try { + const { results: expiredPosts } = await DB.prepare( + `SELECT id, anonymous_user_id, emotion_tag FROM posts WHERE expires_at <= CURRENT_TIMESTAMP` + ).all<{ id: string; anonymous_user_id: string; emotion_tag: string }>(); + + if (!expiredPosts || expiredPosts.length === 0) { + console.log('No expired posts to process.'); + return; + } + + const postIds = expiredPosts.map((p) => p.id); + + // Batch fetch hold counts + const holdsCountsQuery = ` + SELECT post_id, COUNT(id) as hold_count + FROM holds + WHERE post_id IN (${postIds.map(() => '?').join(',')}) + GROUP BY post_id; + `; + const { results: holdsCounts } = await DB.prepare(holdsCountsQuery) + .bind(...postIds) + .all<{ post_id: string; hold_count: number }>(); + + const holdsMap = new Map(); + if (holdsCounts) { + for (const row of holdsCounts) { + holdsMap.set(row.post_id, row.hold_count); + } + } + + // Prepare batch insert for summaries + const summaryStmts = expiredPosts.map((post) => { + const holdCount = holdsMap.get(post.id) || 0; + return DB.prepare( + `INSERT INTO post_summaries (id, anonymous_user_id, emotion_tag, hold_count, dissolved_at) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)` + ).bind(crypto.randomUUID(), + post.anonymous_user_id, + post.emotion_tag, + holdCount + ); + }); + + if (summaryStmts.length > 0) { + await DB.batch(summaryStmts); + console.log(`Created ${summaryStmts.length} post summaries.`); + } + + // Batch delete expired posts + const deleteQuery = `DELETE FROM posts WHERE id IN (${postIds.map(() => '?').join(',')})`; + const deleteResult = await DB.prepare(deleteQuery).bind(...postIds).run(); + + if(deleteResult.success) { + console.log(`Successfully deleted ${deleteResult.meta.changes} expired posts.`); + } else { + console.error(`Failed to delete expired posts: ${deleteResult.error}`); + } + + } catch (e: any) { + console.error('Error in scheduled task:', e.message); + } +}; + + +export default { + fetch: app.fetch, + scheduled: async (event: ScheduledEvent, env: AppContext['Bindings'], ctx: ExecutionContext) => { + ctx.waitUntil(scheduledTask(env)); + }, +}; \ 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..af558b9 --- /dev/null +++ b/backend/src/routes/admin.ts @@ -0,0 +1,238 @@ +import { Hono, MiddlewareHandler } from 'hono'; +import { AppContext, UserData, adminAuthMiddleware } from '../utils'; + +type ReportWithPost = { + reportId: number; + postId: string; + reason: string; + reportedAt: string; + post: { + contentType: string; + contentData: string; + emotionTag: string; + }; +}; + +const adminRoutes = new Hono(); + +// Apply admin authentication to all routes in this file +adminRoutes.use('*', adminAuthMiddleware); + +// Middleware to check for 'admin' role specifically +const adminOnly: MiddlewareHandler = async (c, next) => { + const user = c.get('user'); + if (user?.role !== 'admin') { + return c.json({ success: false, error: 'Forbidden: Admins only.' }, 403); + } + await next(); +}; + +// 1. Get Dashboard Stats +adminRoutes.get('/dashboard', async (c) => { + try { + const activePostsStmt = c.env.DB.prepare("SELECT COUNT(*) as count FROM posts WHERE expires_at > CURRENT_TIMESTAMP"); + const pendingReportsStmt = c.env.DB.prepare("SELECT COUNT(*) as count FROM reports WHERE status = 'pending'"); + const totalUsersStmt = c.env.DB.prepare("SELECT COUNT(*) as count FROM users"); + const postsTodayStmt = c.env.DB.prepare("SELECT COUNT(*) as count FROM posts WHERE created_at >= date('now', '-1 day')"); + + const [activePosts, pendingReports, totalUsers, postsToday] = await c.env.DB.batch<{ count: number }[]>([ + activePostsStmt, + pendingReportsStmt, + totalUsersStmt, + postsTodayStmt + ]); + + return c.json({ + activePosts: activePosts.results[0]?.count ?? 0, + pendingReports: pendingReports.results[0]?.count ?? 0, + totalUsers: totalUsers.results[0]?.count ?? 0, + postsToday: postsToday.results[0]?.count ?? 0 + }); + } catch (e: any) { + console.error("Dashboard stat query failed:", e); + return c.json({ success: false, error: 'Failed to retrieve dashboard stats.' }, 500); + } +}); + +// 2. Get Reported Posts +adminRoutes.get('/reports', async (c) => { + const { status = 'pending', page = '1', limit = '20' } = c.req.query(); + const pageNum = parseInt(page, 10); + const limitNum = parseInt(limit, 10); + const offset = (pageNum - 1) * limitNum; + + try { + const reportsQuery = ` + SELECT r.id as reportId, r.post_id as postId, r.reason, r.created_at as reportedAt, + p.content_type as "post:contentType", p.content_data as "post:contentData", p.emotion_tag as "post:emotionTag" + FROM reports r + JOIN posts p ON r.post_id = p.id + WHERE r.status = ? + ORDER BY r.created_at DESC + LIMIT ? OFFSET ? + `; + const totalQuery = 'SELECT COUNT(*) as count FROM reports WHERE status = ?'; + + const { results: reports } = await c.env.DB.prepare(reportsQuery).bind(status, limitNum, offset).all(); + const totalResult = await c.env.DB.prepare(totalQuery).bind(status).first<{ count: number }>(); + + const formattedReports = reports.map(r => ({ + reportId: r.reportId, + postId: r.postId, + reason: r.reason, + reportedAt: r.reportedAt, + post: { + contentType: r['post:contentType'], + contentData: r['post:contentData'], + emotionTag: r['post:emotionTag'], + } + })); + + return c.json({ + reports: formattedReports, + pagination: { + total: totalResult?.count ?? 0, + page: pageNum, + limit: limitNum, + } + }); + + } catch (e: any) { + console.error("Get reports failed:", e); + return c.json({ success: false, error: 'Failed to retrieve reports.' }, 500); + } +}); + +// 3. Review a Report +adminRoutes.post('/reports/:reportId/review', async (c) => { + const reportId = parseInt(c.req.param('reportId'), 10); + const body = await c.req.json(); + const { action } = body; + + if (!['dismiss', 'delete_post'].includes(action)) { + return c.json({ success: false, error: 'Invalid action specified.' }, 400); + } + + try { + if (action === 'dismiss') { + await c.env.DB.prepare("UPDATE reports SET status = 'reviewed' WHERE id = ?").bind(reportId).run(); + return c.json({ message: 'Report dismissed successfully.' }); + } else if (action === 'delete_post') { + const report = await c.env.DB.prepare("SELECT post_id FROM reports WHERE id = ?").bind(reportId).first<{post_id: string}>(); + if (!report) { + return c.json({ success: false, error: 'Report not found.' }, 404); + } + await c.env.DB.prepare("DELETE FROM posts WHERE id = ?").bind(report.post_id).run(); + // ON DELETE CASCADE handles deleting the report itself. + return c.json({ message: 'Post deleted successfully.' }); + } + return c.json({ success: false, error: 'Action not implemented' }, 501); + } catch(e: any) { + console.error(`Review report ${reportId} failed:`, e); + return c.json({ success: false, error: 'Database operation failed.' }, 500); + } +}); + + +// 4. Manage Admin Users (CRUD) +const usersCrud = new Hono(); +usersCrud.use('*', adminOnly); // Only admins can manage users + +// GET all users +usersCrud.get('/', async (c) => { + const { results } = await c.env.DB.prepare('SELECT email, username, fullName, role, created_at FROM users').all(); + return c.json(results || []); +}); + +// POST a new user +usersCrud.post('/', async (c) => { + const { email, fullName, role = 'moderator' } = await c.req.json(); + if (!email || !fullName) { + return c.json({ success: false, error: 'Email and full name are required.' }, 400); + } + const username = email.split('@')[0] + Math.random().toString(36).substring(2, 6); + + try { + await c.env.DB.prepare('INSERT INTO users (email, username, fullName, role) VALUES (?, ?, ?, ?)') + .bind(email, username, fullName, role).run(); + const newUser = await c.env.DB.prepare('SELECT email, username, fullName, role, created_at FROM users WHERE email = ?').bind(email).first(); + return c.json(newUser, 201); + } catch(e: any) { + if (e.message?.includes('UNIQUE')) { + return c.json({ success: false, error: 'User with this email already exists.' }, 409); + } + return c.json({ success: false, error: 'Could not create user.' }, 500); + } +}); + +// GET a single user by email (PK) +usersCrud.get('/:email', async (c) => { + const { email } = c.req.param(); + const user = await c.env.DB.prepare('SELECT email, username, fullName, role, phone, citizen_id, avatar_url, bio, id_verification_status, created_at FROM users WHERE email = ?').bind(email).first(); + if (!user) { + return c.json({ success: false, error: 'User not found.' }, 404); + } + return c.json(user); +}); + +// PUT (update) a user +usersCrud.put('/:email', async (c) => { + const { email } = c.req.param(); + const { fullName, role, bio } = await c.req.json(); + + try { + const { success } = await c.env.DB.prepare( + `UPDATE users SET + fullName = COALESCE(?, fullName), + role = COALESCE(?, role), + bio = COALESCE(?, bio) + WHERE email = ?` + ).bind(fullName ?? null, role ?? null, bio ?? null, email).run(); + + if (!success) { + return c.json({ success: false, error: 'Failed to update user.' }, 500); + } + const updatedUser = await c.env.DB.prepare('SELECT * FROM users WHERE email = ?').bind(email).first(); + return c.json(updatedUser); + } catch (e: any) { + return c.json({ success: false, error: 'Database update failed.' }, 500); + } +}); + +// DELETE a user +usersCrud.delete('/:email', async (c) => { + const { email } = c.req.param(); + const adminUser = c.get('user'); + + if (adminUser.email === email) { + return c.json({ success: false, error: 'You cannot delete your own account.' }, 403); + } + + const { success } = await c.env.DB.prepare('DELETE FROM users WHERE email = ?').bind(email).run(); + if (!success) { + return c.json({ success: false, error: 'Failed to delete user.' }, 500); + } + return c.body(null, 204); +}); + +adminRoutes.route('/users', usersCrud); + + +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/public.ts b/backend/src/routes/public.ts new file mode 100644 index 0000000..c59ba9a --- /dev/null +++ b/backend/src/routes/public.ts @@ -0,0 +1,309 @@ +import { Hono, MiddlewareHandler } from 'hono'; +import { AppContext, UserData } from '../utils'; + +// Define types based on schema for clarity +type Post = { + id: string; + anonymous_user_id: string; + content_type: 'text' | 'voice' | 'photo'; + content_data: string; + emotion_tag: 'Sadness' | 'Rage' | 'Joy' | 'Anxiety' | 'Love' | 'Grief' | 'Excitement'; + created_at: string; + expires_at: string; +}; + +type PostSummary = { + id: string; + anonymous_user_id: string; + emotion_tag: string; + hold_count: number; + dissolved_at: string; +}; + +const EMOTION_TAGS = ['Sadness', 'Rage', 'Joy', 'Anxiety', 'Love', 'Grief', 'Excitement']; + +const publicRoutes = new Hono(); + +// Middleware to extract and validate the anonymous user ID +const anonymousUserMiddleware: MiddlewareHandler = async (c, next) => { + const anonymousUserId = c.req.header('X-Anonymous-User-ID'); + if (!anonymousUserId) { + return c.json({ success: false, error: 'X-Anonymous-User-ID header is required.' }, 400); + } + c.set('anonymousUserId', anonymousUserId); + await next(); +}; + +// CRITICAL: R2 File upload route (as per general instructions) +publicRoutes.post('/upload', anonymousUserMiddleware, async (c) => { + const body = await c.req.parseBody(); + const file = body['file']; + + if (!(file instanceof File)) { + return c.json({ success: false, error: 'File is required in the form data.' }, 400); + } + + const fileExtension = file.name.split('.').pop()?.toLowerCase() || ''; + const key = `${crypto.randomUUID()}.${fileExtension}`; + + try { + await c.env.R2.put(key, await file.arrayBuffer(), { + httpMetadata: { contentType: file.type }, + }); + const url = `/api/media/${key}`; + return c.json({ success: true, url: url }); + } catch (e: any) { + console.error("R2 Upload failed:", e); + return c.json({ success: false, error: 'Failed to upload file.' }, 500); + } +}); + + +// CRITICAL: R2 Media proxy route (as per general instructions) +publicRoutes.get('/media/:key', async (c) => { + const key = c.req.param('key'); + 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'); + + 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 (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 }); +}) + + +// 2. Create Post +publicRoutes.post('/posts', anonymousUserMiddleware, async (c) => { + const anonymousUserId = c.get('anonymousUserId'); + const body = await c.req.json(); + + const { contentType, contentData, emotionTag } = body; + + if (!contentType || !contentData || !emotionTag) { + return c.json({ success: false, error: 'contentType, contentData, and emotionTag are required.' }, 400); + } + + if (!['text', 'voice', 'photo'].includes(contentType)) { + return c.json({ success: false, error: 'Invalid contentType.' }, 400); + } + + if (!EMOTION_TAGS.includes(emotionTag)) { + return c.json({ success: false, error: 'Invalid emotionTag.' }, 400); + } + + try { + const ttlSetting = await c.env.DB.prepare("SELECT value FROM settings WHERE key = 'default_post_ttl_hours'").first<{ value: string }>(); + const ttlHours = ttlSetting ? parseInt(ttlSetting.value, 10) : 24; + + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + ttlHours); + + const postId = crypto.randomUUID(); + const createdAt = new Date().toISOString(); + const expiresAtISO = expiresAt.toISOString(); + + const { success } = await c.env.DB.prepare( + `INSERT INTO posts (id, anonymous_user_id, content_type, content_data, emotion_tag, created_at, expires_at) + VALUES (?, ?, ?, ?, ?, ?, ?)` + ).bind(postId, anonymousUserId, contentType, contentData, emotionTag, createdAt, expiresAtISO).run(); + + if (!success) { + return c.json({ success: false, error: 'Failed to create post.' }, 500); + } + + const newPost: Partial = { + id: postId, + contentType, + contentData, + emotionTag, + createdAt: createdAt, + expiresAt: expiresAtISO + }; + + return c.json(newPost, 201); + } catch (e: any) { + console.error("Post creation failed:", e); + return c.json({ success: false, error: 'Database operation failed.' }, 500); + } +}); + +// 3. Get Stream of Posts +publicRoutes.get('/posts', async (c) => { + const { emotion, cursor } = c.req.query(); + const limit = 20; + + let query = 'SELECT id, content_type, content_data, emotion_tag, created_at FROM posts WHERE expires_at > CURRENT_TIMESTAMP'; + const bindings: (string | number)[] = []; + + if (emotion && EMOTION_TAGS.includes(emotion)) { + query += ' AND emotion_tag = ?'; + bindings.push(emotion); + } + + if (cursor) { + query += ' AND created_at < ?'; + bindings.push(cursor); + } + + query += ' ORDER BY created_at DESC LIMIT ?'; + bindings.push(limit); + + try { + const { results } = await c.env.DB.prepare(query).bind(...bindings).all(); + const posts = results || []; + + let nextCursor: string | null = null; + if (posts.length === limit) { + nextCursor = posts[posts.length - 1].created_at; + } + + return c.json({ posts, nextCursor }); + } catch (e: any) { + console.error("Get posts failed:", e); + return c.json({ success: false, error: 'Failed to retrieve posts.' }, 500); + } +}); + +// 4. Get User's Own Active Posts +publicRoutes.get('/posts/me', anonymousUserMiddleware, async (c) => { + const anonymousUserId = c.get('anonymousUserId'); + try { + const { results } = await c.env.DB.prepare( + `SELECT p.*, (SELECT COUNT(*) FROM holds h WHERE h.post_id = p.id) as holdCount + FROM posts p + WHERE p.anonymous_user_id = ? AND p.expires_at > CURRENT_TIMESTAMP + ORDER BY p.created_at DESC` + ).bind(anonymousUserId).all(); + + return c.json({ posts: results || [] }); + + } catch (e: any) { + console.error("Get user's posts failed:", e); + return c.json({ success: false, error: 'Failed to retrieve your posts.' }, 500); + } +}); + +// 5. Hold a Post +publicRoutes.post('/posts/:postId/hold', anonymousUserMiddleware, async (c) => { + const anonymousUserId = c.get('anonymousUserId'); + const { postId } = c.req.param(); + + try { + // Check if post exists and is active + const post = await c.env.DB.prepare("SELECT id FROM posts WHERE id = ? AND expires_at > CURRENT_TIMESTAMP").bind(postId).first(); + if (!post) { + return c.json({ success: false, error: 'Post not found or has expired.' }, 404); + } + + const { success } = await c.env.DB.prepare( + 'INSERT INTO holds (post_id, anonymous_user_id) VALUES (?, ?)' + ).bind(postId, anonymousUserId).run(); + + if (!success) { + // This likely means a UNIQUE constraint violation + return c.json({ success: false, error: 'You have already held this post.' }, 409); + } + + return c.body(null, 204); + + } catch (e: any) { + // D1 throws a generic error for constraint violations + if (e.message?.includes('UNIQUE constraint failed')) { + return c.json({ success: false, error: 'You have already held this post.' }, 409); + } + console.error("Hold post failed:", e); + return c.json({ success: false, error: 'Database operation failed.' }, 500); + } +}); + + +// 6. Report a Post +publicRoutes.post('/posts/:postId/report', anonymousUserMiddleware, async (c) => { + const anonymousUserId = c.get('anonymousUserId'); + const { postId } = c.req.param(); + const body = await c.req.json(); + const reason = body.reason || null; + + try { + const post = await c.env.DB.prepare("SELECT id FROM posts WHERE id = ?").bind(postId).first(); + if (!post) { + return c.json({ success: false, error: 'Post not found.' }, 404); + } + + const existingReport = await c.env.DB.prepare("SELECT id FROM reports WHERE post_id = ? AND reporter_anonymous_user_id = ?") + .bind(postId, anonymousUserId) + .first(); + + if (existingReport) { + return c.json({ success: false, error: 'You have already reported this post.' }, 409); + } + + await c.env.DB.prepare( + 'INSERT INTO reports (post_id, reporter_anonymous_user_id, reason) VALUES (?, ?, ?)' + ).bind(postId, anonymousUserId, reason).run(); + + return c.body(null, 202); + } catch (e: any) { + console.error("Report post failed:", e); + return c.json({ success: false, error: 'Database operation failed.' }, 500); + } +}); + +// 7. Get Post Summaries +publicRoutes.get('/posts/summaries', anonymousUserMiddleware, async (c) => { + const anonymousUserId = c.get('anonymousUserId'); + try { + const { results } = await c.env.DB.prepare( + `SELECT id, emotion_tag, hold_count, dissolved_at + FROM post_summaries + WHERE anonymous_user_id = ? + ORDER BY dissolved_at DESC` + ).bind(anonymousUserId).all(); + + return c.json({ summaries: results || [] }); + } catch (e: any) { + console.error("Get summaries failed:", e); + return c.json({ success: false, error: 'Failed to retrieve post summaries.' }, 500); + } +}); + +// 8. Clear Post Summaries +publicRoutes.post('/posts/summaries/clear', anonymousUserMiddleware, async (c) => { + const anonymousUserId = c.get('anonymousUserId'); + const body = await c.req.json(); + const summaryIds = body.summaryIds; + + if (!Array.isArray(summaryIds) || summaryIds.length === 0) { + return c.json({ success: false, error: 'summaryIds must be a non-empty array.' }, 400); + } + + try { + const query = `DELETE FROM post_summaries WHERE anonymous_user_id = ? AND id IN (${summaryIds.map(() => '?').join(',')})`; + await c.env.DB.prepare(query).bind(anonymousUserId, ...summaryIds).run(); + + return c.body(null, 204); + } catch (e: any) { + console.error("Clear summaries failed:", e); + return c.json({ success: false, error: 'Failed to clear summaries.' }, 500); + } +}); + +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..d14f09a --- /dev/null +++ b/backend/src/routes/utils.ts @@ -0,0 +1,168 @@ +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; + S3_ENDPOINT: string; + S3_ACCESS_KEY_ID: string; + S3_SECRET_ACCESS_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', + fullName: 'Admin User', + citizen_id: 'admin-bypass-id', + phone: '9999999999', + role: 'admin' + }; + } + + 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) { + const userData = data.user_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)}`; + + await c.env.DB.prepare(` + INSERT INTO users (email, fullName, mauth_token, phone, username, citizen_id, role) + VALUES (?, ?, ?, ?, ?, ?, ?) + `).bind( + userData.email, + userData.fullName, + token, + userData.phone || null, + defaultUsername, + userData.citizen_id, + 'visitor' + ).run(); + + const newUser = await c.env.DB.prepare('SELECT * FROM users WHERE email = ?').bind(userData.email).first(); + c.set('user', newUser as UserData); + } else { + c.set('user', dbUser as UserData); + } + + c.set('token', token); + await next(); + } catch (err: any) { + return c.json({ success: false, msg: err.message }, 401); + } +}; + +export const adminAuthMiddleware: MiddlewareHandler = async (c, next) => { + const authHeader = c.req.header('Authorization'); + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return c.json({ success: false, msg: 'Missing admin token' }, 401); + } + + const token = authHeader.split(' ')[1]; + + // 1. Try mAuth Agentic Token verification + try { + const verifyRes = await fetch('https://api.mauth.mworld.cloud/mAuthAG/verifyToken', { + headers: { 'mAuthAgenticToken': token } + }); + const verifyData = await verifyRes.json() as any; + if (verifyData.success) { + // Valid Agentic Token + await next(); + return; + } + } catch (err) { + console.error('[Admin Auth] Agentic verification failed:', err); + } + + // 2. Fallback to Legacy Hash-based validation + try { + const encoder = new TextEncoder(); + const data = encoder.encode(token); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + + const result = await c.env.DB.prepare("SELECT value FROM settings WHERE key = 'admin_access_token_hash'") + .first<{ value: string }>(); + + if (result && result.value === hashHex) { + await next(); + return; + } + } catch (err: any) { + console.error('[Admin Auth] Legacy verification failed:', err.message); + } + + return c.json({ success: false, msg: 'Invalid or expired admin 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..d14f09a --- /dev/null +++ b/backend/src/utils.ts @@ -0,0 +1,168 @@ +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; + S3_ENDPOINT: string; + S3_ACCESS_KEY_ID: string; + S3_SECRET_ACCESS_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', + fullName: 'Admin User', + citizen_id: 'admin-bypass-id', + phone: '9999999999', + role: 'admin' + }; + } + + 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) { + const userData = data.user_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)}`; + + await c.env.DB.prepare(` + INSERT INTO users (email, fullName, mauth_token, phone, username, citizen_id, role) + VALUES (?, ?, ?, ?, ?, ?, ?) + `).bind( + userData.email, + userData.fullName, + token, + userData.phone || null, + defaultUsername, + userData.citizen_id, + 'visitor' + ).run(); + + const newUser = await c.env.DB.prepare('SELECT * FROM users WHERE email = ?').bind(userData.email).first(); + c.set('user', newUser as UserData); + } else { + c.set('user', dbUser as UserData); + } + + c.set('token', token); + await next(); + } catch (err: any) { + return c.json({ success: false, msg: err.message }, 401); + } +}; + +export const adminAuthMiddleware: MiddlewareHandler = async (c, next) => { + const authHeader = c.req.header('Authorization'); + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return c.json({ success: false, msg: 'Missing admin token' }, 401); + } + + const token = authHeader.split(' ')[1]; + + // 1. Try mAuth Agentic Token verification + try { + const verifyRes = await fetch('https://api.mauth.mworld.cloud/mAuthAG/verifyToken', { + headers: { 'mAuthAgenticToken': token } + }); + const verifyData = await verifyRes.json() as any; + if (verifyData.success) { + // Valid Agentic Token + await next(); + return; + } + } catch (err) { + console.error('[Admin Auth] Agentic verification failed:', err); + } + + // 2. Fallback to Legacy Hash-based validation + try { + const encoder = new TextEncoder(); + const data = encoder.encode(token); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + + const result = await c.env.DB.prepare("SELECT value FROM settings WHERE key = 'admin_access_token_hash'") + .first<{ value: string }>(); + + if (result && result.value === hashHex) { + await next(); + return; + } + } catch (err: any) { + console.error('[Admin Auth] Legacy verification failed:', err.message); + } + + return c.json({ success: false, msg: 'Invalid or expired admin token' }, 401); +}; \ No newline at end of file diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..9ec9117 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ESNext" + ], + "types": [ + "@cloudflare/workers-types" + ], + "strict": true, + "skipLibCheck": true + } +} \ No newline at end of file diff --git a/backend/wrangler.json b/backend/wrangler.json new file mode 100644 index 0000000..b78ad2e --- /dev/null +++ b/backend/wrangler.json @@ -0,0 +1,21 @@ +{ + "name": "app-xd1uom", + "main": "src/index.ts", + "compatibility_date": "2024-04-01", + "d1_databases": [ + { + "binding": "DB", + "database_name": "db-xd1uom", + "database_id": "23b00c58-c2c5-4f98-abba-ce49c8cfaa0c" + } + ], + "r2_buckets": [ + { + "binding": "R2", + "bucket_name": "bucket-xd1uom" + } + ], + "vars": { + "S3_ENDPOINT": "" + } +} \ No newline at end of file diff --git a/database/wipe_db.sql b/database/wipe_db.sql new file mode 100644 index 0000000..c308dac --- /dev/null +++ b/database/wipe_db.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS "post_summaries"; DROP TABLE IF EXISTS "reports"; DROP TABLE IF EXISTS "holds"; DROP TABLE IF EXISTS "posts"; DROP TABLE IF EXISTS "settings"; DROP TABLE IF EXISTS "users"; \ No newline at end of file diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 0000000..e6c4e94 --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,3 @@ +name = "xd1uom-worker" +main = "src/index.ts" +compatibility_date = "2024-01-01"