diff --git a/cloud-function/.gitignore b/cloud-function/.gitignore index 6b36db1..d542da2 100644 --- a/cloud-function/.gitignore +++ b/cloud-function/.gitignore @@ -4,4 +4,3 @@ node_modules *.log canonicals.json redirects.json -src/**/*.js diff --git a/cloud-function/package-lock.json b/cloud-function/package-lock.json index 7fdf594..d448327 100644 --- a/cloud-function/package-lock.json +++ b/cloud-function/package-lock.json @@ -22,20 +22,18 @@ "sanitize-filename": "^1.6.3" }, "devDependencies": { - "@swc/core": "^1.14.0", "@types/accept-language-parser": "^1.5.8", "@types/cookie": "^1.0.0", "@types/cookie-parser": "^1.4.10", + "@types/express": "^4.17.21", "@types/he": "^1.2.3", "@types/http-proxy": "^1.17.17", "@types/http-server": "^0.12.4", - "cross-env": "^10.1.0", "foreman": "^3.0.1", "http-proxy": "^1.18.1", "http-server": "^14.1.1", "nodemon": "^3.1.10", "npm-run-all": "^4.1.5", - "ts-node": "^10.9.2", "typescript": "^5.9.3" }, "engines": { @@ -100,26 +98,6 @@ "node": ">=6.9.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/@epic-web/invariant": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", - "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", - "dev": true, - "license": "MIT" - }, "node_modules/@google-cloud/functions-framework": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@google-cloud/functions-framework/-/functions-framework-4.0.0.tgz", @@ -143,34 +121,6 @@ "node": ">=10.0.0" } }, - "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.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "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/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -863,260 +813,6 @@ "@opentelemetry/semantic-conventions": "^1.37.0" } }, - "node_modules/@swc/core": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.14.0.tgz", - "integrity": "sha512-oExhY90bes5pDTVrei0xlMVosTxwd/NMafIpqsC4dMbRYZ5KB981l/CX8tMnGsagTplj/RcG9BeRYmV6/J5m3w==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.25" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.14.0", - "@swc/core-darwin-x64": "1.14.0", - "@swc/core-linux-arm-gnueabihf": "1.14.0", - "@swc/core-linux-arm64-gnu": "1.14.0", - "@swc/core-linux-arm64-musl": "1.14.0", - "@swc/core-linux-x64-gnu": "1.14.0", - "@swc/core-linux-x64-musl": "1.14.0", - "@swc/core-win32-arm64-msvc": "1.14.0", - "@swc/core-win32-ia32-msvc": "1.14.0", - "@swc/core-win32-x64-msvc": "1.14.0" - }, - "peerDependencies": { - "@swc/helpers": ">=0.5.17" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.14.0.tgz", - "integrity": "sha512-uHPC8rlCt04nvYNczWzKVdgnRhxCa3ndKTBBbBpResOZsRmiwRAvByIGh599j+Oo6Z5eyTPrgY+XfJzVmXnN7Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.14.0.tgz", - "integrity": "sha512-2SHrlpl68vtePRknv9shvM9YKKg7B9T13tcTg9aFCwR318QTYo+FzsKGmQSv9ox/Ua0Q2/5y2BNjieffJoo4nA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.14.0.tgz", - "integrity": "sha512-SMH8zn01dxt809svetnxpeg/jWdpi6dqHKO3Eb11u4OzU2PK7I5uKS6gf2hx5LlTbcJMFKULZiVwjlQLe8eqtg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.14.0.tgz", - "integrity": "sha512-q2JRu2D8LVqGeHkmpVCljVNltG0tB4o4eYg+dElFwCS8l2Mnt9qurMCxIeo9mgoqz0ax+k7jWtIRHktnVCbjvQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.14.0.tgz", - "integrity": "sha512-uofpVoPCEUjYIv454ZEZ3sLgMD17nIwlz2z7bsn7rl301Kt/01umFA7MscUovFfAK2IRGck6XB+uulMu6aFhKQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.14.0.tgz", - "integrity": "sha512-quTTx1Olm05fBfv66DEBuOsOgqdypnZ/1Bh3yGXWY7ANLFeeRpCDZpljD9BSjdsNdPOlwJmEUZXMHtGm3v1TZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-musl": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.14.0.tgz", - "integrity": "sha512-caaNAu+aIqT8seLtCf08i8C3/UC5ttQujUjejhMcuS1/LoCKtNiUs4VekJd2UGt+pyuuSrQ6dKl8CbCfWvWeXw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.14.0.tgz", - "integrity": "sha512-EeW3jFlT3YNckJ6V/JnTfGcX7UHGyh6/AiCPopZ1HNaGiXVCKHPpVQZicmtyr/UpqxCXLrTgjHOvyMke7YN26A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.14.0.tgz", - "integrity": "sha512-dPai3KUIcihV5hfoO4QNQF5HAaw8+2bT7dvi8E5zLtecW2SfL3mUZipzampXq5FHll0RSCLzlrXnSx+dBRZIIQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.14.0.tgz", - "integrity": "sha512-nm+JajGrTqUA6sEHdghDlHMNfH1WKSiuvljhdmBACW4ta4LC3gKurX2qZuiBARvPkephW9V/i5S8QPY1PzFEqg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@swc/types": { - "version": "0.1.25", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", - "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/accept-language-parser": { "version": "1.5.8", "resolved": "https://registry.npmjs.org/@types/accept-language-parser/-/accept-language-parser-1.5.8.tgz", @@ -1165,9 +861,9 @@ } }, "node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.24.tgz", + "integrity": "sha512-Mbrt4SRlXSTWryOnHAh2d4UQ/E7n9lZyGSi6KgX+4hkuL9soYbLOVXVhnk/ODp12YsGc95f4pOvqywJ6kngUwg==", "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -1177,9 +873,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -1277,9 +973,9 @@ } }, "node_modules/@types/qs": { - "version": "6.9.18", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", - "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", "license": "MIT" }, "node_modules/@types/range-parser": { @@ -1344,9 +1040,9 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -1364,19 +1060,6 @@ "acorn": "^8" } }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -1440,13 +1123,6 @@ "node": ">= 8" } }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -1878,46 +1554,6 @@ "node": ">= 0.4.0" } }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-env": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", - "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@epic-web/invariant": "^1.0.0", - "cross-spawn": "^7.0.6" - }, - "bin": { - "cross-env": "dist/bin/cross-env.js", - "cross-env-shell": "dist/bin/cross-env-shell.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -2052,16 +1688,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/dotenv": { "version": "17.2.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", @@ -3438,13 +3064,6 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3990,16 +3609,6 @@ "node": ">= 0.8" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -4663,29 +4272,6 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/shell-quote": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", @@ -4990,50 +4576,6 @@ "utf8-byte-length": "^1.0.1" } }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, "node_modules/type-fest": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", @@ -5260,13 +4802,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -5328,22 +4863,6 @@ "webidl-conversions": "^3.0.0" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/which-boxed-primitive": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", @@ -5439,16 +4958,6 @@ "engines": { "node": ">=0.4" } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } } } } diff --git a/cloud-function/package.json b/cloud-function/package.json index d86009c..94bf426 100644 --- a/cloud-function/package.json +++ b/cloud-function/package.json @@ -8,17 +8,16 @@ "type": "module", "main": "src/index.js", "scripts": { - "build": "tsc -b", - "build-canonicals": "cross-env NODE_OPTIONS='--no-warnings=ExperimentalWarning --loader ts-node/esm' node src/build-canonicals.ts", - "build-redirects": "cross-env NODE_OPTIONS='--no-warnings=ExperimentalWarning --loader ts-node/esm' node src/build-redirects.ts", - "gcp-build": "npm run build", - "proxy": "cross-env NODE_OPTIONS='--no-warnings=ExperimentalWarning --loader ts-node/esm' node src/proxy.ts", - "server": "npm run build && functions-framework --target=mdnHandler --ignored-routes \"\"", + "build-canonicals": "node src/build-canonicals.js", + "build-redirects": "node src/build-redirects.js", + "check": "tsc --noEmit", + "proxy": "node src/proxy.js", + "server": "functions-framework --target=mdnHandler --ignored-routes \"\"", "server:watch": "nodemon --exec npm run server", "start": "nf start" }, "nodemonConfig": { - "ext": "json,ts", + "ext": "json,js", "watch": [ ".env", "src" @@ -38,20 +37,18 @@ "sanitize-filename": "^1.6.3" }, "devDependencies": { - "@swc/core": "^1.14.0", "@types/accept-language-parser": "^1.5.8", "@types/cookie": "^1.0.0", "@types/cookie-parser": "^1.4.10", + "@types/express": "^4.17.21", "@types/he": "^1.2.3", "@types/http-proxy": "^1.17.17", "@types/http-server": "^0.12.4", - "cross-env": "^10.1.0", "foreman": "^3.0.1", "http-proxy": "^1.18.1", "http-server": "^14.1.1", "nodemon": "^3.1.10", "npm-run-all": "^4.1.5", - "ts-node": "^10.9.2", "typescript": "^5.9.3" }, "engines": { diff --git a/cloud-function/src/app.ts b/cloud-function/src/app.js similarity index 93% rename from cloud-function/src/app.ts rename to cloud-function/src/app.js index 87d027a..b1c999e 100644 --- a/cloud-function/src/app.ts +++ b/cloud-function/src/app.js @@ -1,6 +1,7 @@ +/** @import { Request, Response } from "express" */ + import cookieParser from "cookie-parser"; -import express, { Request, Response } from "express"; -import { Router } from "express"; +import express, { Router } from "express"; import { ANY_ATTACHMENT_EXT } from "./internal/constants/index.js"; @@ -123,8 +124,12 @@ router.get( ); router.all("*", notFound); +/** + * Create the main MDN handler function for Google Cloud Functions + * @returns {(req: Request, res: Response) => Promise} Express-compatible handler + */ export function createHandler() { - return async (req: Request, res: Response) => + return async (req, res) => router(req, res, () => { /* noop */ }); diff --git a/cloud-function/src/build-canonicals.ts b/cloud-function/src/build-canonicals.js similarity index 94% rename from cloud-function/src/build-canonicals.ts rename to cloud-function/src/build-canonicals.js index 3e8e772..48dd54d 100644 --- a/cloud-function/src/build-canonicals.ts +++ b/cloud-function/src/build-canonicals.js @@ -23,7 +23,8 @@ async function buildCanonicals() { const lines = content.split("\n"); const pages = lines.filter((line) => line.startsWith("/")); - const siteMap: Record = {}; + /** @type {Record} */ + const siteMap = {}; for (const page of pages) { siteMap[normalizePath(page)] = page; } diff --git a/cloud-function/src/build-redirects.ts b/cloud-function/src/build-redirects.js similarity index 96% rename from cloud-function/src/build-redirects.ts rename to cloud-function/src/build-redirects.js index c13dffc..543b024 100644 --- a/cloud-function/src/build-redirects.ts +++ b/cloud-function/src/build-redirects.js @@ -15,7 +15,8 @@ dotenv.config({ }); function buildRedirectsMap() { - const redirectMap = new Map(); + /** @type {Map} */ + const redirectMap = new Map(); ["CONTENT_ROOT", "CONTENT_TRANSLATED_ROOT"].forEach((envvar) => { if (!process.env[envvar]) { diff --git a/cloud-function/src/canonicals.ts b/cloud-function/src/canonicals.js similarity index 100% rename from cloud-function/src/canonicals.ts rename to cloud-function/src/canonicals.js diff --git a/cloud-function/src/constants.ts b/cloud-function/src/constants.js similarity index 100% rename from cloud-function/src/constants.ts rename to cloud-function/src/constants.js diff --git a/cloud-function/src/env.ts b/cloud-function/src/env.js similarity index 57% rename from cloud-function/src/env.ts rename to cloud-function/src/env.js index 50e7252..3c11568 100644 --- a/cloud-function/src/env.ts +++ b/cloud-function/src/env.js @@ -1,8 +1,9 @@ +/** @import { Request } from "express" */ + import * as path from "node:path"; import { cwd } from "node:process"; import dotenv from "dotenv"; -import { Request } from "express"; dotenv.config({ path: path.join(cwd(), process.env["ENV_FILE"] || ".env"), @@ -12,33 +13,47 @@ dotenv.config({ export const LOCAL_CONTENT = "http://localhost:8100/"; export const LOCAL_RUMBA = "http://localhost:8000/"; -export enum Origin { - main = "main", - liveSamples = "liveSamples", - play = "play", - unsafe = "unsafe", -} +/** + * @typedef {"main" | "liveSamples" | "play" | "unsafe"} OriginType + */ -export enum Source { - content = "content", - liveSamples = "liveSamples", - api = "rumba", - sharedAssets = "sharedAssets", -} +/** + * @typedef {"content" | "liveSamples" | "rumba" | "sharedAssets"} SourceType + */ + +/** @type {{main: "main", liveSamples: "liveSamples", play: "play", unsafe: "unsafe"}} */ +export const Origin = Object.freeze({ + main: "main", + liveSamples: "liveSamples", + play: "play", + unsafe: "unsafe", +}); + +/** @type {{content: "content", liveSamples: "liveSamples", api: "rumba", sharedAssets: "sharedAssets"}} */ +export const Source = Object.freeze({ + content: "content", + liveSamples: "liveSamples", + api: "rumba", + sharedAssets: "sharedAssets", +}); -export const ORIGIN_MAIN: string = process.env["ORIGIN_MAIN"] || "localhost"; -export const ORIGIN_LIVE_SAMPLES: string = +export const ORIGIN_MAIN = process.env["ORIGIN_MAIN"] || "localhost"; +export const ORIGIN_LIVE_SAMPLES = process.env["ORIGIN_LIVE_SAMPLES"] || "localhost"; -export const ORIGIN_PLAY: string = process.env["ORIGIN_PLAY"] || "localhost"; +export const ORIGIN_PLAY = process.env["ORIGIN_PLAY"] || "localhost"; -export const SOURCE_CONTENT: string = - process.env["SOURCE_CONTENT"] || LOCAL_CONTENT; -export const SOURCE_API: string = +export const SOURCE_CONTENT = process.env["SOURCE_CONTENT"] || LOCAL_CONTENT; +export const SOURCE_API = process.env["SOURCE_API"] || "https://developer.allizom.org/"; -export const SOURCE_SHARED_ASSETS: string = +export const SOURCE_SHARED_ASSETS = process.env["SOURCE_SHARED_ASSETS"] || "https://mdn.github.io/shared-assets/"; -export function getOriginFromRequest(req: Request): Origin { +/** + * Determine the origin type from a request + * @param {Request} req - Express request object + * @returns {OriginType} The origin type + */ +export function getOriginFromRequest(req) { if ( req.hostname === ORIGIN_MAIN && !req.path.includes("/_sample_.") && @@ -57,7 +72,12 @@ export function getOriginFromRequest(req: Request): Origin { } } -export function sourceUri(source: Source): string { +/** + * Get the URI for a given source + * @param {SourceType} source - Source type + * @returns {string} Source URI + */ +export function sourceUri(source) { switch (source) { case Source.content: return SOURCE_CONTENT; @@ -71,8 +91,8 @@ export function sourceUri(source: Source): string { } // Origin trial. -export const ORIGIN_TRIAL_TOKEN: string | undefined = - process.env["ORIGIN_TRIAL_TOKEN"]; +/** @type {string | undefined} */ +export const ORIGIN_TRIAL_TOKEN = process.env["ORIGIN_TRIAL_TOKEN"]; // Kevel. export const KEVEL_SITE_ID = Number(process.env["KEVEL_SITE_ID"] ?? 0); diff --git a/cloud-function/src/handlers/handle-stripe-plans.ts b/cloud-function/src/handlers/handle-stripe-plans.js similarity index 73% rename from cloud-function/src/handlers/handle-stripe-plans.ts rename to cloud-function/src/handlers/handle-stripe-plans.js index aa9b912..061adc6 100644 --- a/cloud-function/src/handlers/handle-stripe-plans.ts +++ b/cloud-function/src/handlers/handle-stripe-plans.js @@ -1,21 +1,36 @@ +/** @import { Request, Response } from "express" */ + import acceptLanguageParser from "accept-language-parser"; -import { Request, Response } from "express"; import { ORIGIN_MAIN } from "../env.js"; import { getRequestCountry } from "../utils.js"; import stageLookup from "../stripe-plans/stage.js"; import prodLookup from "../stripe-plans/prod.js"; -interface PlanResult { - [name: string]: { monthlyPriceInCents: number; id: string }; -} -interface Result { - country: string; - currency: string; - plans: PlanResult; -} +/** + * @typedef {Object} PlanDetails + * @property {number} monthlyPriceInCents + * @property {string} id + */ + +/** + * @typedef {Object.} PlanResult + */ + +/** + * @typedef {Object} Result + * @property {string} country + * @property {string} currency + * @property {PlanResult} plans + */ -export async function handleStripePlans(req: Request, res: Response) { +/** + * Handles requests for Stripe subscription plans based on country and language + * @param {Request} req - Express request object + * @param {Response} res - Express response object + * @returns {Promise} + */ +export async function handleStripePlans(req, res) { const lookupData = ORIGIN_MAIN === "developer.mozilla.org" ? prodLookup : stageLookup; @@ -50,7 +65,8 @@ export async function handleStripePlans(req: Request, res: Response) { return res.sendStatus(500).end(); } - const planResult: PlanResult = {}; + /** @type {PlanResult} */ + const planResult = {}; Object.entries(plans).forEach(([name, plan]) => { let monthlyPriceInCents; if (plan.recurring.interval === "year") { @@ -61,11 +77,12 @@ export async function handleStripePlans(req: Request, res: Response) { planResult[name] = { monthlyPriceInCents, id: plan.price_id }; }); + /** @type {Result} */ const result = { country: countryCode, currency: supportedCurrency.currency, plans: planResult, - } satisfies Result; + }; return ( res diff --git a/cloud-function/src/handlers/proxy-api.ts b/cloud-function/src/handlers/proxy-api.js similarity index 81% rename from cloud-function/src/handlers/proxy-api.ts rename to cloud-function/src/handlers/proxy-api.js index 27ea952..a16418c 100644 --- a/cloud-function/src/handlers/proxy-api.ts +++ b/cloud-function/src/handlers/proxy-api.js @@ -3,6 +3,10 @@ import { createProxyMiddleware, fixRequestBody } from "http-proxy-middleware"; import { Source, sourceUri } from "../env.js"; import { PROXY_TIMEOUT } from "../constants.js"; +/** + * Proxy middleware for API requests + * Forwards requests to the backend API server + */ export const proxyApi = createProxyMiddleware({ target: sourceUri(Source.api), changeOrigin: true, diff --git a/cloud-function/src/handlers/proxy-bsa.ts b/cloud-function/src/handlers/proxy-bsa.js similarity index 91% rename from cloud-function/src/handlers/proxy-bsa.ts rename to cloud-function/src/handlers/proxy-bsa.js index 7dc41e3..a6ffcca 100644 --- a/cloud-function/src/handlers/proxy-bsa.ts +++ b/cloud-function/src/handlers/proxy-bsa.js @@ -1,4 +1,4 @@ -import type { Request, Response } from "express"; +/** @import { Request, Response } from "express" */ import { Coder } from "../internal/pong/index.js"; import { @@ -23,7 +23,14 @@ const handleViewed = createPong2ViewedHandler(coder); const plusLookup = ORIGIN_MAIN === "developer.mozilla.org" ? prodPlusLookup : stagePlusLookup; -export async function proxyBSA(req: Request, res: Response) { +/** + * Handles BSA (BuySellAds) advertising requests + * Supports ad retrieval, click tracking, view tracking, and image proxying + * @param {Request} req - Express request object + * @param {Response} res - Express response object + * @returns {Promise} + */ +export async function proxyBSA(req, res) { const countryCode = getRequestCountry(req); const plusAvailable = countryCode in plusLookup.countryToCurrency; diff --git a/cloud-function/src/handlers/proxy-content-assets.ts b/cloud-function/src/handlers/proxy-content-assets.js similarity index 91% rename from cloud-function/src/handlers/proxy-content-assets.ts rename to cloud-function/src/handlers/proxy-content-assets.js index 5e48416..23d6941 100644 --- a/cloud-function/src/handlers/proxy-content-assets.ts +++ b/cloud-function/src/handlers/proxy-content-assets.js @@ -12,6 +12,10 @@ import { ACTIVE_LOCALES } from "../internal/constants/index.js"; const target = sourceUri(Source.content); +/** + * Proxy middleware for content assets (attachments, media, fonts) + * Falls back to en-US assets for non-English locales, then to production if needed + */ export const proxyContentAssets = createProxyMiddleware({ target, changeOrigin: true, diff --git a/cloud-function/src/handlers/proxy-content.ts b/cloud-function/src/handlers/proxy-content.js similarity index 81% rename from cloud-function/src/handlers/proxy-content.ts rename to cloud-function/src/handlers/proxy-content.js index af5a54f..bbcec43 100644 --- a/cloud-function/src/handlers/proxy-content.ts +++ b/cloud-function/src/handlers/proxy-content.js @@ -10,10 +10,15 @@ import { Source, sourceUri, WILDCARD_ENABLED } from "../env.js"; import { PROXY_TIMEOUT } from "../constants.js"; import { isLiveSampleURL } from "../utils.js"; -const notFoundBufferCache: Record> = {}; +/** @type {Record>} */ +const notFoundBufferCache = {}; const target = sourceUri(Source.content); +/** + * Proxy middleware for content requests + * Handles MDN content with 404 fallback logic and wildcard subdomain support + */ export const proxyContent = createProxyMiddleware({ changeOrigin: true, autoRewrite: true, @@ -65,10 +70,14 @@ export const proxyContent = createProxyMiddleware({ }, }); -async function get404ForLocale( - locale: string -): Promise | string> { - let notFoundBuffer: Promise; +/** + * Fetches the 404 page for a given locale with caching + * @param {string} locale - The locale code (e.g., "en-us") + * @returns {Promise} The 404 page content as a Buffer or fallback string + */ +async function get404ForLocale(locale) { + /** @type {Promise} */ + let notFoundBuffer; if (notFoundBufferCache[locale]) { notFoundBuffer = notFoundBufferCache[locale]; } else { diff --git a/cloud-function/src/handlers/proxy-kevel.ts b/cloud-function/src/handlers/proxy-kevel.js similarity index 88% rename from cloud-function/src/handlers/proxy-kevel.ts rename to cloud-function/src/handlers/proxy-kevel.js index 4ea63b7..a84413b 100644 --- a/cloud-function/src/handlers/proxy-kevel.ts +++ b/cloud-function/src/handlers/proxy-kevel.js @@ -1,5 +1,6 @@ +/** @import { Request, Response } from "express" */ + import { Client } from "@adzerk/decision-sdk"; -import type { Request, Response } from "express"; import { Coder } from "../internal/pong/index.js"; import { @@ -28,7 +29,14 @@ const handleViewed = createPongViewedHandler(coder); const plusLookup = ORIGIN_MAIN === "developer.mozilla.org" ? prodPlusLookup : stagePlusLookup; -export async function proxyKevel(req: Request, res: Response) { +/** + * Handles Kevel (formerly Adzerk) advertising requests + * Supports ad retrieval, click tracking, view tracking, and image proxying + * @param {Request} req - Express request object + * @param {Response} res - Express response object + * @returns {Promise} + */ +export async function proxyKevel(req, res) { const countryCode = getRequestCountry(req); const plusAvailable = countryCode in plusLookup.countryToCurrency; diff --git a/cloud-function/src/handlers/proxy-pong.js b/cloud-function/src/handlers/proxy-pong.js new file mode 100644 index 0000000..0644530 --- /dev/null +++ b/cloud-function/src/handlers/proxy-pong.js @@ -0,0 +1,18 @@ +/** @import { Request, Response } from "express" */ + +import { BSA_ENABLED } from "../env.js"; +import { proxyBSA } from "./proxy-bsa.js"; +import { proxyKevel } from "./proxy-kevel.js"; + +/** + * Routes pong/advertising requests to either BSA or Kevel handler + * @param {Request} req - Express request object + * @param {Response} res - Express response object + * @returns {Promise} + */ +export async function proxyPong(req, res) { + if (BSA_ENABLED) { + return proxyBSA(req, res); + } + return proxyKevel(req, res); +} diff --git a/cloud-function/src/handlers/proxy-pong.ts b/cloud-function/src/handlers/proxy-pong.ts deleted file mode 100644 index e26a90a..0000000 --- a/cloud-function/src/handlers/proxy-pong.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { BSA_ENABLED } from "../env.js"; -import { proxyBSA } from "./proxy-bsa.js"; -import { proxyKevel } from "./proxy-kevel.js"; - -import type { Request, Response } from "express"; - -export async function proxyPong(req: Request, res: Response) { - if (BSA_ENABLED) { - return proxyBSA(req, res); - } - return proxyKevel(req, res); -} diff --git a/cloud-function/src/handlers/proxy-shared-assets.ts b/cloud-function/src/handlers/proxy-shared-assets.js similarity index 88% rename from cloud-function/src/handlers/proxy-shared-assets.ts rename to cloud-function/src/handlers/proxy-shared-assets.js index 0cb03e3..0d937bb 100644 --- a/cloud-function/src/handlers/proxy-shared-assets.ts +++ b/cloud-function/src/handlers/proxy-shared-assets.js @@ -9,6 +9,10 @@ import { PROXY_TIMEOUT } from "../constants.js"; const target = sourceUri(Source.sharedAssets); +/** + * Proxy middleware for shared assets (interactive examples) + * Rewrites paths and sets appropriate cache headers + */ export const proxySharedAssets = createProxyMiddleware({ target, pathRewrite: { diff --git a/cloud-function/src/handlers/proxy-telemetry.ts b/cloud-function/src/handlers/proxy-telemetry.js similarity index 74% rename from cloud-function/src/handlers/proxy-telemetry.ts rename to cloud-function/src/handlers/proxy-telemetry.js index aa87565..8f48969 100644 --- a/cloud-function/src/handlers/proxy-telemetry.ts +++ b/cloud-function/src/handlers/proxy-telemetry.js @@ -2,6 +2,10 @@ import { createProxyMiddleware, fixRequestBody } from "http-proxy-middleware"; import { PROXY_TIMEOUT } from "../constants.js"; +/** + * Proxy middleware for Mozilla telemetry submissions + * Forwards telemetry data to Mozilla's telemetry ingestion service + */ export const proxyTelemetry = createProxyMiddleware({ target: "https://incoming.telemetry.mozilla.org", changeOrigin: true, diff --git a/cloud-function/src/headers.ts b/cloud-function/src/headers.js similarity index 62% rename from cloud-function/src/headers.ts rename to cloud-function/src/headers.js index 14866ab..15d207f 100644 --- a/cloud-function/src/headers.ts +++ b/cloud-function/src/headers.js @@ -1,4 +1,4 @@ -import type { IncomingMessage, ServerResponse } from "node:http"; +/** @import { IncomingMessage, ServerResponse } from "node:http" */ import { CSP_VALUE } from "./internal/constants/index.js"; import { isLiveSampleURL } from "./utils.js"; @@ -11,11 +11,14 @@ const NO_CACHE_VALUE = "no-store"; const HASHED_REGEX = /\.[a-f0-9]{8,32}\./; -export function withContentResponseHeaders( - proxyRes: IncomingMessage, - req: IncomingMessage, - res: ServerResponse -): ServerResponse { +/** + * Add content response headers to a proxy response + * @param {IncomingMessage} proxyRes - Proxy response + * @param {IncomingMessage} req - Request object + * @param {ServerResponse} res - Response object + * @returns {ServerResponse} Response object with headers + */ +export function withContentResponseHeaders(proxyRes, req, res) { if (res.headersSent) { console.warn( `Cannot set content response headers. Headers already sent for: ${req.url}` @@ -53,7 +56,13 @@ export function withContentResponseHeaders( return res; } -function getCacheControl(statusCode: number, url: string) { +/** + * Get cache control header value based on status code and URL + * @param {number} statusCode - HTTP status code + * @param {string} url - Request URL + * @returns {string | null} Cache control value + */ +function getCacheControl(statusCode, url) { if ( statusCode === 404 || url.endsWith("/service-worker.js") || @@ -74,23 +83,41 @@ function getCacheControl(statusCode: number, url: string) { return null; } -function getCacheMaxAgeForUrl(url: string): number { +/** + * Get cache max age for a URL + * @param {string} url - Request URL + * @returns {number} Max age in seconds + */ +function getCacheMaxAgeForUrl(url) { const isHashed = HASHED_REGEX.test(url); const maxAge = isHashed ? HASHED_MAX_AGE : DEFAULT_MAX_AGE; return maxAge; } -function parseContentType(value: unknown): string { +/** + * Parse content type from header value + * @param {unknown} value - Content type header value + * @returns {string} Parsed content type + */ +function parseContentType(value) { const firstValue = Array.isArray(value) ? (value[0] ?? "") : value; return typeof firstValue === "string" ? firstValue : ""; } +/** + * Set content response headers using a setter function + * @param {(name: string, value: string) => void} setHeader - Header setter function + * @param {object} options - Header options + * @param {boolean} [options.csp=true] - Whether to set CSP header + * @param {boolean} [options.xFrame=true] - Whether to set X-Frame-Options header + * @returns {void} + */ export function setContentResponseHeaders( - setHeader: (name: string, value: string) => void, - { csp = true, xFrame = true }: { csp?: boolean; xFrame?: boolean } -): void { + setHeader, + { csp = true, xFrame = true } = {} +) { [ ["Referrer-Policy", "strict-origin-when-cross-origin"], ["X-Content-Type-Options", "nosniff"], diff --git a/cloud-function/src/index.ts b/cloud-function/src/index.js similarity index 100% rename from cloud-function/src/index.ts rename to cloud-function/src/index.js diff --git a/cloud-function/src/internal/constants/index.ts b/cloud-function/src/internal/constants/index.js similarity index 95% rename from cloud-function/src/internal/constants/index.ts rename to cloud-function/src/internal/constants/index.js index 98e6a2f..3b8aafc 100644 --- a/cloud-function/src/internal/constants/index.ts +++ b/cloud-function/src/internal/constants/index.js @@ -196,7 +196,11 @@ export const CSP_DIRECTIVES = { "worker-src": ["'self'"], }; -export const cspToString = (csp: Record) => +/** + * @param {Record} csp + * @returns {string} + */ +export const cspToString = (csp) => Object.entries(csp) .map(([directive, values]) => `${directive} ${values.join(" ")};`) .join(" "); @@ -224,7 +228,11 @@ export const ANY_ATTACHMENT_EXT = [ ...VIDEO_EXT, ].sort(); -export function createRegExpFromExtensions(...extensions: string[]) { +/** + * @param {...string} extensions + * @returns {RegExp} + */ +export function createRegExpFromExtensions(...extensions) { return new RegExp(`\\.(${extensions.join("|")})$`, "i"); } diff --git a/cloud-function/src/internal/constants/index.test.ts b/cloud-function/src/internal/constants/index.test.js similarity index 100% rename from cloud-function/src/internal/constants/index.test.ts rename to cloud-function/src/internal/constants/index.test.js diff --git a/cloud-function/src/internal/fundamental-redirects/index.ts b/cloud-function/src/internal/fundamental-redirects/index.js similarity index 98% rename from cloud-function/src/internal/fundamental-redirects/index.ts rename to cloud-function/src/internal/fundamental-redirects/index.js index 7f39960..9d0ac61 100644 --- a/cloud-function/src/internal/fundamental-redirects/index.ts +++ b/cloud-function/src/internal/fundamental-redirects/index.js @@ -10,6 +10,14 @@ const startRe = /^\^?\/?/; const startTemplate = /^\//; const LOCALE_PATTERN = "(?:[a-zA-Z]{2}|eng)(?:-[a-zA-Z]{2})?"; +/** + * @param {RegExp} pattern + * @param {string | Function} template + * @param {Object} [options={}] + * @param {boolean} [options.permanent] + * @param {boolean} [options.colonToSlash] + * @returns {Function} + */ function redirect(pattern, template, options = {}) { return (path) => { const match = pattern.exec(path); @@ -31,6 +39,13 @@ function redirect(pattern, template, options = {}) { }; } +/** + * @param {RegExp} pattern + * @param {string | Function} template + * @param {Object} [options={}] + * @param {boolean} [options.prependLocale=true] + * @returns {Function} + */ function localeRedirect( pattern, template, @@ -52,6 +67,12 @@ function localeRedirect( return redirect(patternWithLocale, _template, options); } +/** + * @param {RegExp} pattern + * @param {string | Function} template + * @param {Object} [options={}] + * @returns {Function} + */ function externalRedirect(pattern, template, options = {}) { return localeRedirect(pattern, template, { prependLocale: false, @@ -831,8 +852,19 @@ const zoneRedirects = [ ["Marketplace", "Mozilla/بازار", ["fa"]], ]; +/** + * @param {string} prefix + * @param {string} zoneRootPattern + * @returns {RegExp} + */ const zonePatternFmt = (prefix, zoneRootPattern) => new RegExp(`^${prefix}${zoneRootPattern}(?:\\/?|(?[\\/$].+))$`, "i"); + +/** + * @param {string} prefix + * @param {string} wikiSlug + * @returns {Function} + */ const subPathFmt = (prefix, wikiSlug) => ({ subPath = "" } = {}) => @@ -1206,6 +1238,10 @@ const REDIRECT_PATTERNS = [].concat( const STARTING_SLASH = /^\//; const ABSOLUTE_URL = /^https?:\/\/.*/; +/** + * @param {string} path + * @returns {Object} + */ export function resolveFundamental(path) { if (ABSOLUTE_URL.exec(path)) { return {}; diff --git a/cloud-function/src/internal/locale-utils/index.ts b/cloud-function/src/internal/locale-utils/index.js similarity index 62% rename from cloud-function/src/internal/locale-utils/index.ts rename to cloud-function/src/internal/locale-utils/index.js index ffc8e2a..d747f9b 100644 --- a/cloud-function/src/internal/locale-utils/index.ts +++ b/cloud-function/src/internal/locale-utils/index.js @@ -1,8 +1,8 @@ +/** @import { Request } from "express" */ + import { parse } from "cookie"; import acceptLanguageParser from "accept-language-parser"; -import { Request } from "express"; - import { DEFAULT_LOCALE, VALID_LOCALES, @@ -11,9 +11,13 @@ import { const VALID_LOCALES_LIST = [...VALID_LOCALES.values()]; -// From https://github.com/aws-samples/cloudfront-authorization-at-edge/blob/01c1bc843d478977005bde86f5834ce76c479eec/src/lambda-edge/shared/shared.ts#L216 -// but rewritten in JavaScript (from TypeScript). -function extractCookiesFromHeaders(headers: { cookie?: any }) { +/** + * From https://github.com/aws-samples/cloudfront-authorization-at-edge/blob/01c1bc843d478977005bde86f5834ce76c479eec/src/lambda-edge/shared/shared.ts#L216 + * but rewritten in JavaScript (from TypeScript). + * @param {{ cookie?: any }} headers + * @returns {Record} + */ +function extractCookiesFromHeaders(headers) { let value = headers["cookie"]; // Cookies are present in the HTTP header "Cookie" that may be present multiple times. @@ -34,19 +38,32 @@ function extractCookiesFromHeaders(headers: { cookie?: any }) { // eslint-disable-next-line unicorn/no-array-reduce const cookies = value.reduce( - (reduced: Record, header: { value: any }) => + /** + * @param {Record} reduced + * @param {{ value: string }} header + */ + (reduced, header) => Object.assign(reduced, parse(header.value)), - {} + /** @type {Record} */ ({}) ); return cookies; } -function getCookie(request: Request, cookieKey: string) { - return extractCookiesFromHeaders(request.headers as any)[cookieKey]; +/** + * @param {Request} request + * @param {string} cookieKey + * @returns {string | undefined} + */ +function getCookie(request, cookieKey) { + return extractCookiesFromHeaders(request.headers)[cookieKey]; } -function getAcceptLanguage(request: Request) { +/** + * @param {Request} request + * @returns {string | null} + */ +function getAcceptLanguage(request) { const acceptLangHeaders = request.headers["accept-language"]; if (typeof acceptLangHeaders === "string") { @@ -58,14 +75,19 @@ function getAcceptLanguage(request: Request) { return value; } -export function getLocale(request: Request, fallback = DEFAULT_LOCALE) { +/** + * @param {Request} request + * @param {string} [fallback] + * @returns {string} + */ +export function getLocale(request, fallback = DEFAULT_LOCALE) { // First try by cookie. const cookieLocale = getCookie(request, PREFERRED_LOCALE_COOKIE_NAME); if ( cookieLocale && // If it's valid, stick to it. VALID_LOCALES.has(cookieLocale.toLowerCase()) ) { - return VALID_LOCALES.get(cookieLocale.toLowerCase()); + return /** @type {string} */ (VALID_LOCALES.get(cookieLocale.toLowerCase())); } // Each header in request.headers is always a list of objects. @@ -76,6 +98,10 @@ export function getLocale(request: Request, fallback = DEFAULT_LOCALE) { return locale || fallback; } -export function isValidLocale(locale: any) { +/** + * @param {any} locale + * @returns {boolean} + */ +export function isValidLocale(locale) { return typeof locale === "string" && VALID_LOCALES.has(locale.toLowerCase()); } diff --git a/cloud-function/src/internal/play/index.ts b/cloud-function/src/internal/play/index.js similarity index 98% rename from cloud-function/src/internal/play/index.ts rename to cloud-function/src/internal/play/index.js index 073a012..b85ea83 100644 --- a/cloud-function/src/internal/play/index.ts +++ b/cloud-function/src/internal/play/index.js @@ -45,6 +45,7 @@ export function withRunnerResponseHeaders(req, res) { /** * @param {Record} csp + * @returns {string} */ function cspToString(csp) { return Object.entries(csp) @@ -80,7 +81,8 @@ export const PLAYGROUND_UNSAFE_CSP_VALUE = cspToString({ /** * @param {TemplateStringsArray} strings - * @param {any[]} args + * @param {...any} args + * @returns {string} */ function html(strings, ...args) { return strings @@ -92,6 +94,7 @@ function html(strings, ...args) { * @param {State | null} state * @param {string} hrefWithCode * @param {string} searchWithState + * @returns {string} */ export function renderWarning(state, hrefWithCode, searchWithState) { const { css, html: htmlCode, js } = state || { css: "", html: "", js: "" }; @@ -218,6 +221,7 @@ export function renderWarning(state, hrefWithCode, searchWithState) { /** * @param {State | null} [state=null] + * @returns {string} */ export function renderHtml(state = null) { const { @@ -531,6 +535,7 @@ export function renderHtml(state = null) { /** * @param {Theme | undefined} [theme] + * @returns {string} */ function renderBlank(theme) { return html` @@ -588,6 +593,7 @@ function renderThemeStyles(theme) { /** * @param {string | null} base64String + * @returns {Promise<{state: string | null, hash: string | null}>} * * This is the Node.js version of `client/src/playground/utils.ts`. Keep in sync! */ @@ -615,8 +621,8 @@ export async function decompressFromBase64(base64String) { const ORIGIN_PLAY_SUFFIX = `.${ORIGIN_PLAY}`; /** - * * @param {string} hostname + * @returns {string} */ function playSubdomain(hostname) { if (hostname.endsWith(ORIGIN_PLAY_SUFFIX)) { @@ -627,6 +633,7 @@ function playSubdomain(hostname) { /** * @param {string} hostname + * @returns {boolean} */ function isMDNHost(hostname) { return ( @@ -640,6 +647,7 @@ function isMDNHost(hostname) { /** * @param {express.Request} req * @param {express.Response} res + * @returns {Promise} */ export async function handleRunner(req, res) { const url = new URL(req.url, "https://example.com"); diff --git a/cloud-function/src/internal/pong/cc2ip.ts b/cloud-function/src/internal/pong/cc2ip.js similarity index 98% rename from cloud-function/src/internal/pong/cc2ip.ts rename to cloud-function/src/internal/pong/cc2ip.js index e1ef207..34a595f 100644 --- a/cloud-function/src/internal/pong/cc2ip.ts +++ b/cloud-function/src/internal/pong/cc2ip.js @@ -178,6 +178,10 @@ export const cc2ip = { ZW: "41.174.104.223", }; +/** + * @param {string} countryCode + * @returns {string} + */ export default function anonymousIpByCC(countryCode) { return cc2ip[countryCode] ?? "10.10.10.10"; } diff --git a/cloud-function/src/internal/pong/coding.ts b/cloud-function/src/internal/pong/coding.js similarity index 87% rename from cloud-function/src/internal/pong/coding.ts rename to cloud-function/src/internal/pong/coding.js index 192b202..d5bc98b 100644 --- a/cloud-function/src/internal/pong/coding.ts +++ b/cloud-function/src/internal/pong/coding.js @@ -15,6 +15,10 @@ export class Coder { constructor(signSecret) { this.signSecret = signSecret; } + /** + * @param {string} [s=""] + * @returns {string} + */ encodeAndSign(s = "") { const hmac = createHmac("sha256", this.signSecret); hmac.update(s); @@ -23,6 +27,10 @@ export class Coder { )}`; } + /** + * @param {string} [tuple=""] + * @returns {string | null} + */ decodeAndVerify(tuple = "") { if (tuple === null) { return null; diff --git a/cloud-function/src/internal/pong/image.ts b/cloud-function/src/internal/pong/image.js similarity index 69% rename from cloud-function/src/internal/pong/image.ts rename to cloud-function/src/internal/pong/image.js index 2378860..8dbd4ab 100644 --- a/cloud-function/src/internal/pong/image.ts +++ b/cloud-function/src/internal/pong/image.js @@ -1,5 +1,9 @@ // @ts-nocheck /* global fetch */ +/** + * @param {string} src + * @returns {Promise<{status: number, buf: ArrayBuffer, contentType: string | null}>} + */ export async function fetchImage(src) { const res = await fetch(src); const status = res.status; diff --git a/cloud-function/src/internal/pong/index.ts b/cloud-function/src/internal/pong/index.js similarity index 100% rename from cloud-function/src/internal/pong/index.ts rename to cloud-function/src/internal/pong/index.js diff --git a/cloud-function/src/internal/pong/pong.ts b/cloud-function/src/internal/pong/pong.js similarity index 94% rename from cloud-function/src/internal/pong/pong.ts rename to cloud-function/src/internal/pong/pong.js index 83e2162..40e8950 100644 --- a/cloud-function/src/internal/pong/pong.ts +++ b/cloud-function/src/internal/pong/pong.js @@ -13,6 +13,12 @@ const PLACEMENTS = { // Allow list for client sent keywords. const ALLOWED_KEYWORDS = []; +/** + * @param {any} client + * @param {any} coder + * @param {any} env + * @returns {Function} + */ export function createPongGetHandler(client, coder, env) { return async (body, countryCode, userAgent) => { const { keywords = [], pongs = null } = body; @@ -92,6 +98,10 @@ export function createPongGetHandler(client, coder, env) { }; } +/** + * @param {any} coder + * @returns {Function} + */ export function createPongClickHandler(coder) { return async (params) => { const click = coder.decodeAndVerify(params.get("code")); @@ -103,6 +113,10 @@ export function createPongClickHandler(coder) { }; } +/** + * @param {any} coder + * @returns {Function} + */ export function createPongViewedHandler(coder) { return async (params) => { const view = coder.decodeAndVerify(params.get("code")); diff --git a/cloud-function/src/internal/pong/pong2.ts b/cloud-function/src/internal/pong/pong2.js similarity index 97% rename from cloud-function/src/internal/pong/pong2.ts rename to cloud-function/src/internal/pong/pong2.js index 7b93f63..51ee8f8 100644 --- a/cloud-function/src/internal/pong/pong2.ts +++ b/cloud-function/src/internal/pong/pong2.js @@ -2,6 +2,10 @@ import he from "he"; import anonymousIpByCC from "./cc2ip.js"; +/** + * @param {string | number | any} hash + * @returns {string | undefined} + */ function fixupColor(hash) { if (typeof hash !== "string" && typeof hash !== "number") { return undefined; @@ -12,6 +16,12 @@ function fixupColor(hash) { } } +/** + * @param {any} zoneKeys + * @param {any} coder + * @param {any} env + * @returns {Function} + */ export function createPong2GetHandler(zoneKeys, coder, env) { return async (body, countryCode, userAgent) => { let { pongs = null } = body; @@ -296,6 +306,10 @@ export function createPong2GetHandler(zoneKeys, coder, env) { }; } +/** + * @param {any} coder + * @returns {Function} + */ export function createPong2ClickHandler(coder) { return async (params, countryCode, userAgent) => { const code = params.get("code"); @@ -331,6 +345,10 @@ export function createPong2ClickHandler(coder) { }; } +/** + * @param {any} coder + * @returns {Function} + */ export function createPong2ViewedHandler(coder) { return async (params, countryCode, userAgent) => { const code = params.get("code"); @@ -357,6 +375,10 @@ export function createPong2ViewedHandler(coder) { }; } +/** + * @param {string} payload + * @returns {URL} + */ function createURL(payload) { if (payload.startsWith("//")) { // BSA omitted 'https:' until May 2024. diff --git a/cloud-function/src/internal/pong/validate-cc2ip.ts b/cloud-function/src/internal/pong/validate-cc2ip.js similarity index 100% rename from cloud-function/src/internal/pong/validate-cc2ip.ts rename to cloud-function/src/internal/pong/validate-cc2ip.js diff --git a/cloud-function/src/internal/slug-utils/index.ts b/cloud-function/src/internal/slug-utils/index.js similarity index 67% rename from cloud-function/src/internal/slug-utils/index.ts rename to cloud-function/src/internal/slug-utils/index.js index b86f897..6394a65 100644 --- a/cloud-function/src/internal/slug-utils/index.ts +++ b/cloud-function/src/internal/slug-utils/index.js @@ -1,6 +1,11 @@ import sanitizeFilename from "sanitize-filename"; -export function slugToFolder(slug: string, joiner = "/") { +/** + * @param {string} slug + * @param {string} [joiner="/"] + * @returns {string} + */ +export function slugToFolder(slug, joiner = "/") { return ( slug // We have slugs with these special characters that would be @@ -14,17 +19,25 @@ export function slugToFolder(slug: string, joiner = "/") { .toLowerCase() .split("/") - .map(input => sanitizeFilename(input)) + .map((input) => sanitizeFilename(input)) .join(joiner) ); } -export function decodePath(path: string) { +/** + * @param {string} path + * @returns {string} + */ +export function decodePath(path) { const decoded = path.split("/").map((element) => decodeURIComponent(element)).join("/"); return decoded; } -export function encodePath(path: string) { +/** + * @param {string} path + * @returns {string} + */ +export function encodePath(path) { const decoded = path.split("/").map((element) => encodeURIComponent(element)).join("/"); return decoded; } diff --git a/cloud-function/src/middlewares/lowercase-pathname.js b/cloud-function/src/middlewares/lowercase-pathname.js new file mode 100644 index 0000000..fd8dfd8 --- /dev/null +++ b/cloud-function/src/middlewares/lowercase-pathname.js @@ -0,0 +1,21 @@ +/** @import { NextFunction, Request, Response } from "express" */ + +/** + * Middleware that lowercases the pathname of the request URL. + * Also updates originalUrl to support http-proxy-middleware v2. + * @param {Request} req - Express request + * @param {Response} _res - Express response + * @param {NextFunction} next - Express next function + * @returns {Promise} + */ +export async function lowercasePathname(req, _res, next) { + const urlParsed = new URL(req.url, `${req.protocol}://${req.headers.host}`); + if (urlParsed.pathname) { + urlParsed.pathname = urlParsed.pathname.toLowerCase(); + req.url = urlParsed.toString(); + // Workaround for http-proxy-middleware v2 using `req.originalUrl`. + // See: https://github.com/chimurai/http-proxy-middleware/pull/731 + req.originalUrl = req.url; + } + next(); +} diff --git a/cloud-function/src/middlewares/lowercase-pathname.ts b/cloud-function/src/middlewares/lowercase-pathname.ts deleted file mode 100644 index 0d311b3..0000000 --- a/cloud-function/src/middlewares/lowercase-pathname.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NextFunction, Request, Response } from "express"; - -export async function lowercasePathname( - req: Request, - _res: Response, - next: NextFunction -) { - const urlParsed = new URL(req.url, `${req.protocol}://${req.headers.host}`); - if (urlParsed.pathname) { - urlParsed.pathname = urlParsed.pathname.toLowerCase(); - req.url = urlParsed.toString(); - // Workaround for http-proxy-middleware v2 using `req.originalUrl`. - // See: https://github.com/chimurai/http-proxy-middleware/pull/731 - req.originalUrl = req.url; - } - next(); -} diff --git a/cloud-function/src/middlewares/not-found.js b/cloud-function/src/middlewares/not-found.js new file mode 100644 index 0000000..89732cb --- /dev/null +++ b/cloud-function/src/middlewares/not-found.js @@ -0,0 +1,11 @@ +/** @import { Request, Response } from "express" */ + +/** + * Middleware that sends a 404 Not Found response. + * @param {Request} _req - Express request + * @param {Response} res - Express response + * @returns {Promise} + */ +export async function notFound(_req, res) { + res.sendStatus(404).end(); +} diff --git a/cloud-function/src/middlewares/not-found.ts b/cloud-function/src/middlewares/not-found.ts deleted file mode 100644 index 71f0f6e..0000000 --- a/cloud-function/src/middlewares/not-found.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Request, Response } from "express"; - -export async function notFound(_req: Request, res: Response) { - res.sendStatus(404).end(); -} diff --git a/cloud-function/src/middlewares/redirect-enforce-trailing-slash.ts b/cloud-function/src/middlewares/redirect-enforce-trailing-slash.js similarity index 53% rename from cloud-function/src/middlewares/redirect-enforce-trailing-slash.ts rename to cloud-function/src/middlewares/redirect-enforce-trailing-slash.js index 32d1b82..cc0a39e 100644 --- a/cloud-function/src/middlewares/redirect-enforce-trailing-slash.ts +++ b/cloud-function/src/middlewares/redirect-enforce-trailing-slash.js @@ -1,13 +1,17 @@ -import { NextFunction, Request, Response } from "express"; +/** @import { NextFunction, Request, Response } from "express" */ import { THIRTY_DAYS } from "../constants.js"; import { isAsset, redirect } from "../utils.js"; -export async function redirectEnforceTrailingSlash( - req: Request, - res: Response, - next: NextFunction -) { +/** + * Middleware that enforces trailing slashes on non-asset URLs. + * Redirects URLs without trailing slashes to the same URL with a trailing slash. + * @param {Request} req - Express request + * @param {Response} res - Express response + * @param {NextFunction} next - Express next function + * @returns {Promise} + */ +export async function redirectEnforceTrailingSlash(req, res, next) { const url = new URL(req.url, `${req.protocol}://${req.headers.host}`); const requestURI = url.pathname; const qs = url.search; diff --git a/cloud-function/src/middlewares/redirect-fundamental.ts b/cloud-function/src/middlewares/redirect-fundamental.js similarity index 68% rename from cloud-function/src/middlewares/redirect-fundamental.ts rename to cloud-function/src/middlewares/redirect-fundamental.js index 5e43cab..1dd655d 100644 --- a/cloud-function/src/middlewares/redirect-fundamental.ts +++ b/cloud-function/src/middlewares/redirect-fundamental.js @@ -1,14 +1,18 @@ -import { NextFunction, Request, Response } from "express"; +/** @import { NextFunction, Request, Response } from "express" */ import { THIRTY_DAYS } from "../constants.js"; import { resolveFundamental } from "../internal/fundamental-redirects/index.js"; import { redirect } from "../utils.js"; -export async function redirectFundamental( - req: Request, - res: Response, - next: NextFunction -) { +/** + * Middleware that handles fundamental redirects from the redirect mapping. + * Preserves query strings when redirecting. + * @param {Request} req - Express request + * @param {Response} res - Express response + * @param {NextFunction} next - Express next function + * @returns {Promise} + */ +export async function redirectFundamental(req, res, next) { const url = new URL(req.url, `${req.protocol}://${req.headers.host}`); const requestURI = req.path; diff --git a/cloud-function/src/middlewares/redirect-leading-slash.ts b/cloud-function/src/middlewares/redirect-leading-slash.js similarity index 60% rename from cloud-function/src/middlewares/redirect-leading-slash.ts rename to cloud-function/src/middlewares/redirect-leading-slash.js index 910c0ba..1a6c343 100644 --- a/cloud-function/src/middlewares/redirect-leading-slash.ts +++ b/cloud-function/src/middlewares/redirect-leading-slash.js @@ -1,4 +1,4 @@ -import { NextFunction, Request, Response } from "express"; +/** @import { NextFunction, Request, Response } from "express" */ import { redirect } from "../utils.js"; @@ -14,11 +14,16 @@ import { redirect } from "../utils.js"; // Prevent any pathnames that start with a double //. // This essentially means that a request for `GET /////anything` becomes // 302 with `Location: /anything`. -export async function redirectLeadingSlash( - req: Request, - res: Response, - next: NextFunction -) { + +/** + * Middleware that prevents security issues from multiple leading slashes. + * Normalizes pathnames that start with multiple slashes to a single slash. + * @param {Request} req - Express request + * @param {Response} res - Express response + * @param {NextFunction} next - Express next function + * @returns {Promise} + */ +export async function redirectLeadingSlash(req, res, next) { const pathname = req.url; const normalizedPathname = normalizeLeadingSlash(pathname); if (pathname !== normalizedPathname) { @@ -28,6 +33,11 @@ export async function redirectLeadingSlash( next(); } -function normalizeLeadingSlash(pathname: string): string { +/** + * Normalizes multiple leading slashes to a single slash. + * @param {string} pathname - The pathname to normalize + * @returns {string} The normalized pathname + */ +function normalizeLeadingSlash(pathname) { return pathname.replace(/^(\/|%2f)+/i, "/"); } diff --git a/cloud-function/src/middlewares/redirect-locale.ts b/cloud-function/src/middlewares/redirect-locale.js similarity index 81% rename from cloud-function/src/middlewares/redirect-locale.ts rename to cloud-function/src/middlewares/redirect-locale.js index 999c268..f158c87 100644 --- a/cloud-function/src/middlewares/redirect-locale.ts +++ b/cloud-function/src/middlewares/redirect-locale.js @@ -1,4 +1,4 @@ -import { NextFunction, Request, Response } from "express"; +/** @import { NextFunction, Request, Response } from "express" */ import { getLocale } from "../internal/locale-utils/index.js"; import { VALID_LOCALES } from "../internal/constants/index.js"; @@ -7,11 +7,15 @@ import { redirect } from "../utils.js"; const NEEDS_LOCALE = /^\/(?:blog|curriculum|docs|play|search|settings|plus)(?:$|\/)/; -export async function redirectLocale( - req: Request, - res: Response, - next: NextFunction -) { +/** + * Middleware that handles locale-related redirects. + * Inserts missing locales and corrects locale casing. + * @param {Request} req - Express request + * @param {Response} res - Express response + * @param {NextFunction} next - Express next function + * @returns {Promise} + */ +export async function redirectLocale(req, res, next) { const url = new URL(req.url, `${req.protocol}://${req.headers.host}`); const requestURI = url.pathname; const requestURILowerCase = requestURI.toLowerCase(); diff --git a/cloud-function/src/middlewares/redirect-moved-pages.ts b/cloud-function/src/middlewares/redirect-moved-pages.js similarity index 72% rename from cloud-function/src/middlewares/redirect-moved-pages.ts rename to cloud-function/src/middlewares/redirect-moved-pages.js index 284dd60..21d8128 100644 --- a/cloud-function/src/middlewares/redirect-moved-pages.ts +++ b/cloud-function/src/middlewares/redirect-moved-pages.js @@ -1,20 +1,25 @@ -import { createRequire } from "node:module"; +/** @import { NextFunction, Request, Response } from "express" */ -import { NextFunction, Request, Response } from "express"; +import { createRequire } from "node:module"; import { decodePath } from "../internal/slug-utils/index.js"; import { THIRTY_DAYS } from "../constants.js"; import { redirect } from "../utils.js"; const require = createRequire(import.meta.url); +/** @type {Record} */ const REDIRECTS = require("../../redirects.json"); const REDIRECT_SUFFIXES = ["/index.json", "/bcd.json", ""]; -export async function redirectMovedPages( - req: Request, - res: Response, - next: NextFunction -) { +/** + * Middleware that handles redirects for moved pages. + * Uses the redirects.json mapping generated from _redirects.txt files. + * @param {Request} req - Express request + * @param {Response} res - Express response + * @param {NextFunction} next - Express next function + * @returns {Promise} + */ +export async function redirectMovedPages(req, res, next) { // Important: The requestURI may be URI-encoded. // Example: // - Encoded: /zh-TW/docs/AJAX:%E4%B8%8A%E6%89%8B%E7%AF%87 diff --git a/cloud-function/src/middlewares/redirect-non-canonicals.ts b/cloud-function/src/middlewares/redirect-non-canonicals.js similarity index 64% rename from cloud-function/src/middlewares/redirect-non-canonicals.ts rename to cloud-function/src/middlewares/redirect-non-canonicals.js index 5739098..0d287f6 100644 --- a/cloud-function/src/middlewares/redirect-non-canonicals.ts +++ b/cloud-function/src/middlewares/redirect-non-canonicals.js @@ -1,4 +1,4 @@ -import { NextFunction, Request, Response } from "express"; +/** @import { NextFunction, Request, Response } from "express" */ import { THIRTY_DAYS } from "../constants.js"; import { normalizePath, redirect } from "../utils.js"; @@ -6,11 +6,15 @@ import { CANONICALS } from "../canonicals.js"; const REDIRECT_SUFFIXES = ["/index.json", "/bcd.json", ""]; -export async function redirectNonCanonicals( - req: Request, - res: Response, - next: NextFunction -) { +/** + * Middleware that redirects non-canonical URLs to their canonical versions. + * Handles URLs with different suffixes (index.json, bcd.json, or none). + * @param {Request} req - Express request + * @param {Response} res - Express response + * @param {NextFunction} next - Express next function + * @returns {Promise} + */ +export async function redirectNonCanonicals(req, res, next) { const parsedUrl = new URL(req.url, `${req.protocol}://${req.headers.host}/`); const { pathname } = parsedUrl; @@ -44,7 +48,13 @@ export async function redirectNonCanonicals( next(); } -function joinPath(a: string, b: string) { +/** + * Joins two path segments, handling overlapping slashes. + * @param {string} a - First path segment + * @param {string} b - Second path segment + * @returns {string} The joined path + */ +function joinPath(a, b) { if (a.endsWith("/") && b.startsWith("/")) { return a + b.slice(1); } else { diff --git a/cloud-function/src/middlewares/redirect-preferred-locale.ts b/cloud-function/src/middlewares/redirect-preferred-locale.js similarity index 62% rename from cloud-function/src/middlewares/redirect-preferred-locale.ts rename to cloud-function/src/middlewares/redirect-preferred-locale.js index 40a2360..2b81dd1 100644 --- a/cloud-function/src/middlewares/redirect-preferred-locale.ts +++ b/cloud-function/src/middlewares/redirect-preferred-locale.js @@ -1,12 +1,17 @@ -import { NextFunction, Request, Response } from "express"; +/** @import { NextFunction, Request, Response } from "express" */ + import { normalizePath, redirect } from "../utils.js"; import { CANONICALS } from "../canonicals.js"; -export async function redirectPreferredLocale( - req: Request, - res: Response, - next: NextFunction -) { +/** + * Middleware that redirects to the user's preferred locale if available. + * Checks the 'preferredlocale' cookie and redirects if the content exists in that locale. + * @param {Request} req - Express request + * @param {Response} res - Express response + * @param {NextFunction} next - Express next function + * @returns {Promise} + */ +export async function redirectPreferredLocale(req, res, next) { // Check 1: Does the user prefer a locale and has redirect enabled? const preferredLocale = req.cookies["preferredlocale"]; @@ -39,7 +44,12 @@ export async function redirectPreferredLocale( next(); } -function localeAndSlugOf(url: URL): [string, string] { +/** + * Extracts the locale and slug from a URL. + * @param {URL} url - The URL to parse + * @returns {[string, string]} A tuple of [locale, slug] + */ +function localeAndSlugOf(url) { const locale = url.pathname.split("/").at(1) || ""; const slug = url.pathname.split("/").slice(2).join("/"); diff --git a/cloud-function/src/middlewares/redirect-trailing-slash.ts b/cloud-function/src/middlewares/redirect-trailing-slash.js similarity index 85% rename from cloud-function/src/middlewares/redirect-trailing-slash.ts rename to cloud-function/src/middlewares/redirect-trailing-slash.js index f0a94b4..e2d1ece 100644 --- a/cloud-function/src/middlewares/redirect-trailing-slash.ts +++ b/cloud-function/src/middlewares/redirect-trailing-slash.js @@ -1,4 +1,4 @@ -import { NextFunction, Request, Response } from "express"; +/** @import { NextFunction, Request, Response } from "express" */ import { THIRTY_DAYS } from "../constants.js"; import { VALID_LOCALES } from "../internal/constants/index.js"; @@ -25,11 +25,16 @@ const LEGACY_URI_NEEDING_TRAILING_SLASH = new RegExp( )})?/(?:account|contribute|maintenance-mode|payments)/?$` ); -export async function redirectTrailingSlash( - req: Request, - res: Response, - next: NextFunction -) { +/** + * Middleware that handles trailing slash redirects. + * Home pages require trailing slashes, all other pages should not have them. + * Special handling for locale home pages and legacy URIs. + * @param {Request} req - Express request + * @param {Response} res - Express response + * @param {NextFunction} next - Express next function + * @returns {Promise} + */ +export async function redirectTrailingSlash(req, res, next) { const url = new URL(req.url, `${req.protocol}://${req.headers.host}`); let requestURI = url.pathname; const requestURILowerCase = requestURI.toLowerCase(); diff --git a/cloud-function/src/middlewares/require-origin.js b/cloud-function/src/middlewares/require-origin.js new file mode 100644 index 0000000..f60eea1 --- /dev/null +++ b/cloud-function/src/middlewares/require-origin.js @@ -0,0 +1,26 @@ +/** @import { NextFunction, Request, Response } from "express" */ +/** @import { OriginType } from "../env.js" */ + +import { WILDCARD_ENABLED, getOriginFromRequest } from "../env.js"; + +/** + * Creates a middleware that requires the request to come from one of the expected origins. + * Returns 404 if the origin doesn't match (unless WILDCARD_ENABLED is true). + * @param {...OriginType} expectedOrigins - The allowed origin types + * @returns {(req: Request, res: Response, next: NextFunction) => void} Express middleware function + */ +export function requireOrigin(...expectedOrigins) { + return async (req, res, next) => { + if (WILDCARD_ENABLED) { + return next(); + } + + const actualOrigin = getOriginFromRequest(req); + + if (expectedOrigins.includes(actualOrigin)) { + return next(); + } else { + return res.sendStatus(404).end(); + } + }; +} diff --git a/cloud-function/src/middlewares/require-origin.ts b/cloud-function/src/middlewares/require-origin.ts deleted file mode 100644 index 79dd79d..0000000 --- a/cloud-function/src/middlewares/require-origin.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { NextFunction, Request, Response } from "express"; - -import { Origin, WILDCARD_ENABLED, getOriginFromRequest } from "../env.js"; - -export function requireOrigin(...expectedOrigins: Origin[]) { - return async (req: Request, res: Response, next: NextFunction) => { - if (WILDCARD_ENABLED) { - return next(); - } - - const actualOrigin = getOriginFromRequest(req); - - if (expectedOrigins.includes(actualOrigin)) { - return next(); - } else { - return res.sendStatus(404).end(); - } - }; -} diff --git a/cloud-function/src/middlewares/resolve-index-html.ts b/cloud-function/src/middlewares/resolve-index-html.js similarity index 57% rename from cloud-function/src/middlewares/resolve-index-html.ts rename to cloud-function/src/middlewares/resolve-index-html.js index 5594d64..9a2e29b 100644 --- a/cloud-function/src/middlewares/resolve-index-html.ts +++ b/cloud-function/src/middlewares/resolve-index-html.js @@ -1,15 +1,20 @@ -import * as path from "node:path"; +/** @import { NextFunction, Request, Response } from "express" */ -import { NextFunction, Request, Response } from "express"; +import * as path from "node:path"; import { slugToFolder } from "../internal/slug-utils/index.js"; import { isAsset } from "../utils.js"; -export async function resolveIndexHTML( - req: Request, - _res: Response, - next: NextFunction -) { +/** + * Middleware that resolves document URLs to index.html paths. + * Converts URL slugs to folder paths and appends index.html for non-asset requests. + * Also updates originalUrl to support http-proxy-middleware v2. + * @param {Request} req - Express request + * @param {Response} _res - Express response + * @param {NextFunction} next - Express next function + * @returns {Promise} + */ +export async function resolveIndexHTML(req, _res, next) { const urlParsed = new URL(req.url, `${req.protocol}://${req.headers.host}`); if (urlParsed.pathname) { let pathname = slugToFolder(urlParsed.pathname); diff --git a/cloud-function/src/middlewares/stripForwardedHostHeaders.js b/cloud-function/src/middlewares/stripForwardedHostHeaders.js new file mode 100644 index 0000000..b7bd749 --- /dev/null +++ b/cloud-function/src/middlewares/stripForwardedHostHeaders.js @@ -0,0 +1,19 @@ +/** @import { NextFunction, Request, Response } from "express" */ + +// Don't strip other `X-Forwarded-*` headers. +const HEADER_REGEXP = /^(x-forwarded-host|forwarded)$/i; + +/** + * Middleware that strips X-Forwarded-Host and Forwarded headers from requests. + * Other X-Forwarded-* headers are left intact. + * @param {Request} req - Express request + * @param {Response} _res - Express response + * @param {NextFunction} next - Express next function + * @returns {Promise} + */ +export async function stripForwardedHostHeaders(req, _res, next) { + Object.keys(req.headers) + .filter((name) => HEADER_REGEXP.test(name)) + .forEach((name) => delete req.headers[name]); + next(); +} diff --git a/cloud-function/src/middlewares/stripForwardedHostHeaders.ts b/cloud-function/src/middlewares/stripForwardedHostHeaders.ts deleted file mode 100644 index 3d15b26..0000000 --- a/cloud-function/src/middlewares/stripForwardedHostHeaders.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { NextFunction, Request, Response } from "express"; - -// Don't strip other `X-Forwarded-*` headers. -const HEADER_REGEXP = /^(x-forwarded-host|forwarded)$/i; - -export async function stripForwardedHostHeaders( - req: Request, - _res: Response, - next: NextFunction -) { - Object.keys(req.headers) - .filter((name) => HEADER_REGEXP.test(name)) - .forEach((name) => delete req.headers[name]); - next(); -} diff --git a/cloud-function/src/proxy.ts b/cloud-function/src/proxy.js similarity index 100% rename from cloud-function/src/proxy.ts rename to cloud-function/src/proxy.js diff --git a/cloud-function/src/stripe-plans/index.ts b/cloud-function/src/stripe-plans/index.d.ts similarity index 100% rename from cloud-function/src/stripe-plans/index.ts rename to cloud-function/src/stripe-plans/index.d.ts diff --git a/cloud-function/src/stripe-plans/prod.ts b/cloud-function/src/stripe-plans/prod.js similarity index 99% rename from cloud-function/src/stripe-plans/prod.ts rename to cloud-function/src/stripe-plans/prod.js index 6fa3d7a..9627991 100644 --- a/cloud-function/src/stripe-plans/prod.ts +++ b/cloud-function/src/stripe-plans/prod.js @@ -1,6 +1,7 @@ -import type { LookupData } from "./index.js"; +/** @import { LookupData } from "./index.d.ts" */ -const plans: LookupData = { +/** @type {LookupData} */ +const plans = { countryToCurrency: { AS: { currency: "usd", diff --git a/cloud-function/src/stripe-plans/stage.ts b/cloud-function/src/stripe-plans/stage.js similarity index 99% rename from cloud-function/src/stripe-plans/stage.ts rename to cloud-function/src/stripe-plans/stage.js index 1d329b7..db52195 100644 --- a/cloud-function/src/stripe-plans/stage.ts +++ b/cloud-function/src/stripe-plans/stage.js @@ -1,6 +1,7 @@ -import type { LookupData } from "./index.js"; +/** @import { LookupData } from "./index.d.ts" */ -const plans: LookupData = { +/** @type {LookupData} */ +const plans = { countryToCurrency: { AS: { currency: "usd", diff --git a/cloud-function/src/utils.ts b/cloud-function/src/utils.js similarity index 58% rename from cloud-function/src/utils.ts rename to cloud-function/src/utils.js index 893570e..cbc950a 100644 --- a/cloud-function/src/utils.ts +++ b/cloud-function/src/utils.js @@ -1,4 +1,4 @@ -import { Request, Response } from "express"; +/** @import { Request, Response } from "express" */ import { ANY_ATTACHMENT_EXT, @@ -7,7 +7,12 @@ import { import { DEFAULT_COUNTRY } from "./constants.js"; -export function getRequestCountry(req: Request): string { +/** + * Get the country code from the request headers + * @param {Request} req - Express request object + * @returns {string} Two-letter country code + */ +export function getRequestCountry(req) { const value = req.headers["cloudfront-viewer-country"]; if (typeof value === "string" && value !== "ZZ") { @@ -17,11 +22,20 @@ export function getRequestCountry(req: Request): string { } } +/** + * Send a redirect response with appropriate caching headers + * @param {Response} res - Express response object + * @param {string} location - Redirect location URL + * @param {object} options - Redirect options + * @param {number} [options.status=302] - HTTP status code + * @param {number} [options.cacheControlSeconds=0] - Cache duration in seconds + * @returns {void} + */ export function redirect( - res: Response, - location: string, + res, + location, { status = 302, cacheControlSeconds = 0 } = {} -): void { +) { let cacheControlValue; if (cacheControlSeconds) { cacheControlValue = `max-age=${cacheControlSeconds},public`; @@ -44,7 +58,12 @@ export function redirect( res.set("Cache-Control", cacheControlValue).redirect(status, newLocation); } -export function isLiveSampleURL(url: string) { +/** + * Check if a URL is a live sample URL + * @param {string} url - URL to check + * @returns {boolean} True if the URL contains '/_sample_.' + */ +export function isLiveSampleURL(url) { return url.includes("/_sample_."); } @@ -56,10 +75,20 @@ const ANY_ATTACHMENT_REGEXP = createRegExpFromExtensions( ...TEXT_EXT ); -export function isAsset(url: string) { +/** + * Check if a URL points to an asset file + * @param {string} url - URL to check + * @returns {boolean} True if the URL matches asset file extensions + */ +export function isAsset(url) { return ANY_ATTACHMENT_REGEXP.test(url); } -export function normalizePath(path: string) { +/** + * Normalize a path by lowercasing and removing trailing slash + * @param {string} path - Path to normalize + * @returns {string} Normalized path + */ +export function normalizePath(path) { return path.toLowerCase().replace(/\/$/, ""); } diff --git a/cloud-function/tsconfig.json b/cloud-function/tsconfig.json index c96df50..82a3fba 100644 --- a/cloud-function/tsconfig.json +++ b/cloud-function/tsconfig.json @@ -1,6 +1,5 @@ { "$schema": "https://json.schemastore.org/tsconfig", - "display": "Node 18 + ESM + Strictest", "compilerOptions": { "lib": ["es2022", "DOM"], "module": "NodeNext", @@ -20,10 +19,9 @@ "noUncheckedIndexedAccess": true, "noUnusedLocals": true, "noUnusedParameters": true, - "checkJs": true - }, - "ts-node": { - "esm": true, - "swc": true + "checkJs": true, + "allowJs": true, + "maxNodeModuleJsDepth": 0, + "noEmit": true } }