From e64350a0a15a0de60a81c456aaa49ef20d4d6010 Mon Sep 17 00:00:00 2001 From: Florian Merz Date: Fri, 3 Mar 2023 17:55:41 +0100 Subject: [PATCH 001/343] wip --- gcp/function/.gcloudignore | 17 + gcp/function/.gitignore | 3 + gcp/function/package-lock.json | 1470 ++++++++++++++++++++++ gcp/function/package.json | 21 + gcp/function/src/app.ts | 32 + gcp/function/src/cli.ts | 13 + gcp/function/src/env.ts | 86 ++ gcp/function/src/handlers/bcdApi.ts | 26 + gcp/function/src/handlers/client.ts | 26 + gcp/function/src/handlers/content.ts | 34 + gcp/function/src/handlers/liveSamples.ts | 10 + gcp/function/src/handlers/rumba.ts | 13 + gcp/function/src/handlers/spa.ts | 16 + gcp/function/src/headers.ts | 23 + gcp/function/src/index.ts | 4 + gcp/function/src/source.ts | 16 + gcp/function/tsconfig.json | 33 + 17 files changed, 1843 insertions(+) create mode 100644 gcp/function/.gcloudignore create mode 100644 gcp/function/.gitignore create mode 100644 gcp/function/package-lock.json create mode 100644 gcp/function/package.json create mode 100644 gcp/function/src/app.ts create mode 100644 gcp/function/src/cli.ts create mode 100644 gcp/function/src/env.ts create mode 100644 gcp/function/src/handlers/bcdApi.ts create mode 100644 gcp/function/src/handlers/client.ts create mode 100644 gcp/function/src/handlers/content.ts create mode 100644 gcp/function/src/handlers/liveSamples.ts create mode 100644 gcp/function/src/handlers/rumba.ts create mode 100644 gcp/function/src/handlers/spa.ts create mode 100644 gcp/function/src/headers.ts create mode 100644 gcp/function/src/index.ts create mode 100644 gcp/function/src/source.ts create mode 100644 gcp/function/tsconfig.json diff --git a/gcp/function/.gcloudignore b/gcp/function/.gcloudignore new file mode 100644 index 000000000000..5a616cba337e --- /dev/null +++ b/gcp/function/.gcloudignore @@ -0,0 +1,17 @@ +# This file specifies files that are *not* uploaded to Google Cloud +# using gcloud. It follows the same syntax as .gitignore, with the addition of +# "#!include" directives (which insert the entries of the given .gitignore-style +# file at that point). +# +# For more information, run: +# $ gcloud topic gcloudignore +# +.gcloudignore +# If you would like to upload your .git directory, .gitignore file or files +# from your .gitignore file, remove the corresponding line +# below: +.git +.gitignore + +node_modules +#!include:.gitignore diff --git a/gcp/function/.gitignore b/gcp/function/.gitignore new file mode 100644 index 000000000000..7086d62cf2a9 --- /dev/null +++ b/gcp/function/.gitignore @@ -0,0 +1,3 @@ +.env +src/**/*.js +src/internal diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json new file mode 100644 index 000000000000..e38b120d4f18 --- /dev/null +++ b/gcp/function/package-lock.json @@ -0,0 +1,1470 @@ +{ + "name": "mdn-function", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mdn-function", + "version": "0.0.1", + "license": "MPL-2.0", + "dependencies": { + "@google-cloud/functions-framework": "^3.1.3", + "@yari-internal/constants": "file:src/internal/libs/constants", + "@yari-internal/fundamental-redirects": "file:src/internals/fundamental-redirects", + "@yari-internal/locale-utils": "file:src/internal/libs/locale-utils", + "@yari-internal/slug-utils": "file:src/internal/libs/slug-utils", + "dotenv": "^16.0.3", + "express": "^4.18.2", + "http-proxy": "^1.18.1", + "sanitize-filename": "^1.6.3" + } + }, + "../../../libs/constants": { + "extraneous": true + }, + "../../../libs/fundamental-redirects": { + "extraneous": true + }, + "../../../libs/locale-utils": { + "extraneous": true + }, + "../../../libs/slug-utils": { + "extraneous": true + }, + "../../libs/constants": { + "name": "@yari-internal/constants", + "version": "0.0.1", + "extraneous": true, + "license": "MPL-2.0" + }, + "../../libs/fundamental-redirects": { + "name": "@yari-internal/fundamental-redirects", + "version": "0.0.1", + "extraneous": true, + "license": "MPL-2.0" + }, + "../../libs/locale-utils": { + "name": "@yari-internal/locale-utils", + "version": "0.0.1", + "extraneous": true, + "license": "MPL-2.0" + }, + "../../libs/slug-utils": { + "name": "@yari-internal/slug-utils", + "version": "0.0.1", + "extraneous": true, + "license": "MPL-2.0", + "dependencies": { + "sanitize-filename": "^1.6.3" + } + }, + "../libs/constants": { + "extraneous": true + }, + "../libs/fundamental-redirects": { + "extraneous": true + }, + "../libs/locale-utils": { + "extraneous": true + }, + "../libs/slug-utils": { + "extraneous": true + }, + "node_modules/@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@google-cloud/functions-framework": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@google-cloud/functions-framework/-/functions-framework-3.1.3.tgz", + "integrity": "sha512-gNKEkbud/+QkS5+vn+UEvBX6L522OpyTOjM83/cR1JRhSsBTNn3kPe4L17ZQMjJHFvO9xIxF3FaPDTfOx1Umpw==", + "dependencies": { + "@types/express": "4.17.13", + "body-parser": "^1.18.3", + "cloudevents": "^6.0.0", + "express": "^4.16.4", + "minimist": "^1.2.7", + "on-finished": "^2.3.0", + "read-pkg-up": "^7.0.1", + "semver": "^7.3.5" + }, + "bin": { + "functions-framework": "build/src/main.js", + "functions-framework-nodejs": "build/src/main.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.33", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", + "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/mime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" + }, + "node_modules/@types/node": { + "version": "18.14.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.5.tgz", + "integrity": "sha512-CRT4tMK/DHYhw1fcCEBwME9CSaZNclxfzVMe7GsO6ULSwsttbj70wSiX6rZdIjGblu93sTJxLdhNIT85KKI7Qw==" + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==" + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "node_modules/@types/serve-static": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", + "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", + "dependencies": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@yari-internal/constants": { + "resolved": "src/internal/libs/constants", + "link": true + }, + "node_modules/@yari-internal/fundamental-redirects": { + "resolved": "src/internals/fundamental-redirects", + "link": true + }, + "node_modules/@yari-internal/locale-utils": { + "resolved": "src/internal/libs/locale-utils", + "link": true + }, + "node_modules/@yari-internal/slug-utils": { + "resolved": "src/internal/libs/slug-utils", + "link": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cloudevents": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/cloudevents/-/cloudevents-6.0.4.tgz", + "integrity": "sha512-Vay81bTsutFkZxHnM2K0rev95d0x7aTZ3G+Bmm8/GnIzsVtGfeBkLcXFD4czZ08RoOn6POKl+rIXaBS+Xn+jIA==", + "dependencies": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "process": "^0.11.10", + "util": "^0.12.4", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=12 <20.0.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", + "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", + "integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==" + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "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", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "src/internal/libs/constants": { + "version": "0.0.1", + "license": "MPL-2.0" + }, + "src/internal/libs/locale-utils": { + "version": "0.0.1", + "license": "MPL-2.0" + }, + "src/internal/libs/slug-utils": { + "version": "0.0.1", + "license": "MPL-2.0", + "dependencies": { + "sanitize-filename": "^1.6.3" + } + }, + "src/internals/fundamental-redirects": {} + } +} diff --git a/gcp/function/package.json b/gcp/function/package.json new file mode 100644 index 000000000000..9deeb9772370 --- /dev/null +++ b/gcp/function/package.json @@ -0,0 +1,21 @@ +{ + "name": "mdn-function", + "version": "0.0.1", + "description": "", + "main": "src/index.js", + "type": "module", + "scripts": {}, + "author": "", + "license": "MPL-2.0", + "dependencies": { + "@yari-internal/constants": "file:src/internal/libs/constants", + "@yari-internal/fundamental-redirects": "file:src/internals/fundamental-redirects", + "@yari-internal/locale-utils": "file:src/internal/libs/locale-utils", + "@yari-internal/slug-utils": "file:src/internal/libs/slug-utils", + "@google-cloud/functions-framework": "^3.1.3", + "dotenv": "^16.0.3", + "express": "^4.18.2", + "http-proxy": "^1.18.1", + "sanitize-filename": "^1.6.3" + } +} diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts new file mode 100644 index 000000000000..4ca59e2fa12f --- /dev/null +++ b/gcp/function/src/app.ts @@ -0,0 +1,32 @@ +import { docs } from "./handlers/content.js"; +import { client } from "./handlers/client.js"; + +import type express from "express"; +import { Router } from "express"; +import { Origin, origin } from "./env.js"; +import { liveSamples } from "./handlers/liveSamples.js"; +import { bcdApi } from "./handlers/bcdApi.js"; +import { spa } from "./handlers/spa.js"; +import { rumba } from "./handlers/rumba.js"; + +const mainRouter = Router(); +mainRouter.get("/bcd/api/*", bcdApi()); +mainRouter.all("/api/*", rumba); +mainRouter.all("/users/fxa/*", rumba); +mainRouter.get("/[^/]+/plus/*", spa); +mainRouter.get("/[^/]+/docs/*", docs()); +mainRouter.get("*", client()); + +export async function handler(req: express.Request, res: express.Response) { + const rPath = req.path; + const reqOrigin = origin(req); + if (reqOrigin === Origin.main && !rPath.includes("/_sample_.")) { + return mainRouter(req, res, () => { + console.error("must not be called"); + }); + } else if (reqOrigin === Origin.liveSamples) { + return liveSamples(req, res); + } else { + return res.status(404).send(); + } +} diff --git a/gcp/function/src/cli.ts b/gcp/function/src/cli.ts new file mode 100644 index 000000000000..29ea0a6f572c --- /dev/null +++ b/gcp/function/src/cli.ts @@ -0,0 +1,13 @@ +import express from "express"; +import { handler } from "./app.js"; + +const contentApp = express(); +const contentPort = 3000; + +contentApp.all("*", handler); +contentApp.listen(contentPort, () => { + console.log(`Content app listening on port ${contentPort}`); +}); +contentApp.listen(5042, () => { + console.log(`Sample app listening on port ${5042}`); +}); diff --git a/gcp/function/src/env.ts b/gcp/function/src/env.ts new file mode 100644 index 000000000000..68cae3e0821e --- /dev/null +++ b/gcp/function/src/env.ts @@ -0,0 +1,86 @@ +import type express from "express"; +import dotenv from "dotenv"; +import * as path from "node:path"; +import { cwd } from "node:process"; + +dotenv.config({ + path: path.join(cwd(), process.env["ENV_FILE"] || ".env"), +}); + +export enum Origin { + main = "main", + liveSamples = "liveSamples", + interactiveSamples = "interactiveSamples", + unsafe = "unsafe", +} + +export enum RuntimeEnv { + prod = "prod", + stage = "stage", + dev = "dev", + local = "local", +} + +export enum Source { + content = "content", + client = "client", + liveSamples = "liveSamples", + interactiveSamples = "interactiveSamples", + bcdApi = "bcdApi", + bcdUpdates = "bcdUpdates", + rumba = "rumba", +} + +export const RUNTIME_ENV: string = process.env["RUNTIME_ENV"] || "prod"; +export const ORIGIN_MAIN: string = + process.env["ORIGIN_MAIN"] || "developer.mozilla.org"; +export const ORIGIN_LIVE_SAMPLES: string = + process.env["ORIGIN_LIVE_SAMPLES"] || "interactive-examples.mdn.mozilla.net"; +export const ORIGIN_INTERACTIVE_SAMPLES: string = + process.env["ORIGIN_INTERACTIVE_SAMPLES"] || + "yari-demos.prod.mdn.mozit.cloud"; + +export function origin(req: express.Request): Origin { + switch (req.hostname) { + case ORIGIN_MAIN: + return Origin.main; + case ORIGIN_LIVE_SAMPLES: + return Origin.liveSamples; + case ORIGIN_INTERACTIVE_SAMPLES: + return Origin.interactiveSamples; + default: + return Origin.unsafe; + } +} + +export const SOURCE_CONTENT: string = + process.env["SOURCE_CONTENT"] || + process.env["BUILD_OUT_ROOT"] || + "https://developer.mozilla.org"; +export const SOURCE_LIVE_SAMPLES: string = + process.env["SOURCE_LIVE_SAMPLES"] || + process.env["BUILD_OUT_ROOT"] || + "https://yari-demos.prod.mdn.mozit.cloud"; +export const SOURCE_BCD_API: string = + process.env["SOURCE_BCD_API"] || "https://developer.mozilla.org"; +export const SOURCE_CLIENT: string = + process.env["SOURCE_CLIENT"] || "https://developer.mozilla.org"; +export const SOURCE_RUMBA: string = + process.env["SOURCE_RUMBA"] || "https://developer.mozilla.org"; + +export function sourceUri(source: Source): string { + switch (source) { + case Source.content: + return SOURCE_CONTENT; + case Source.bcdApi: + return SOURCE_BCD_API; + case Source.client: + return SOURCE_CLIENT; + case Source.liveSamples: + return SOURCE_LIVE_SAMPLES; + case Source.rumba: + return SOURCE_RUMBA; + default: + return ""; + } +} diff --git a/gcp/function/src/handlers/bcdApi.ts b/gcp/function/src/handlers/bcdApi.ts new file mode 100644 index 000000000000..177543e8e541 --- /dev/null +++ b/gcp/function/src/handlers/bcdApi.ts @@ -0,0 +1,26 @@ +import type * as express from "express"; +import httpProxy from "http-proxy"; +import * as path from "node:path"; +import { Source } from "../env.js"; +import { responder } from "../source.js"; + +export function bcdApi(): express.Handler { + return responder({ + source: Source.bcdApi, + http(source) { + const bcdProxy = httpProxy.createProxy({ + changeOrigin: true, + target: source, + autoRewrite: true, + }); + return (req, res) => bcdProxy.web(req, res); + }, + file(source) { + return (req, res) => { + const rPath = req.path; + const filePath = path.join(source, rPath); + res.sendFile(filePath); + }; + }, + }); +} diff --git a/gcp/function/src/handlers/client.ts b/gcp/function/src/handlers/client.ts new file mode 100644 index 000000000000..e2cc59d7b0e2 --- /dev/null +++ b/gcp/function/src/handlers/client.ts @@ -0,0 +1,26 @@ +import type express from "express"; +import httpProxy from "http-proxy"; +import * as path from "node:path"; +import { Source } from "../env.js"; +import { responder } from "../source.js"; + +export function client(): express.Handler { + return responder({ + source: Source.client, + http(source) { + const clientProxy = httpProxy.createProxy({ + changeOrigin: true, + target: source, + autoRewrite: true, + }); + return (req, res) => clientProxy.web(req, res); + }, + file(source) { + return (req, res) => { + const rPath = req.path; + const filePath = path.join(source, rPath); + res.sendFile(filePath); + }; + }, + }); +} diff --git a/gcp/function/src/handlers/content.ts b/gcp/function/src/handlers/content.ts new file mode 100644 index 000000000000..45942041efc4 --- /dev/null +++ b/gcp/function/src/handlers/content.ts @@ -0,0 +1,34 @@ +import type express from "express"; +import * as path from "node:path"; +import { slugToFolder } from "@yari-internal/slug-utils"; +import { withResponseHeaders } from "../headers.js"; +import { responder } from "../source.js"; +import { Source } from "../env.js"; +import httpProxy from "http-proxy"; + +export function docs(): express.Handler { + return responder({ + source: Source.content, + http(source) { + const contentProxy = httpProxy.createProxy({ + changeOrigin: true, + target: source, + autoRewrite: true, + }); + return (req, res) => contentProxy.web(req, res); + }, + file(source) { + return (req, res) => { + const rPath = req.path; + const folderName = slugToFolder(rPath); + let filePath = path.join(source, folderName); + if (path.extname(filePath) === "") { + filePath = path.join(filePath, "index.html"); + } + return withResponseHeaders(res, { csp: true, xFrame: true }).sendFile( + filePath + ); + }; + }, + }); +} diff --git a/gcp/function/src/handlers/liveSamples.ts b/gcp/function/src/handlers/liveSamples.ts new file mode 100644 index 000000000000..9634841d4f04 --- /dev/null +++ b/gcp/function/src/handlers/liveSamples.ts @@ -0,0 +1,10 @@ +import type express from "express"; +import * as path from "node:path"; +import { slugToFolder } from "@yari-internal/slug-utils"; + +export async function liveSamples(req: express.Request, res: express.Response) { + const rPath = req.path; + const folderName = slugToFolder(rPath); + const filePath = path.join("/tmp/231/", folderName); + res.sendFile(filePath); +} diff --git a/gcp/function/src/handlers/rumba.ts b/gcp/function/src/handlers/rumba.ts new file mode 100644 index 000000000000..981f5f70e112 --- /dev/null +++ b/gcp/function/src/handlers/rumba.ts @@ -0,0 +1,13 @@ +import httpProxy from "http-proxy"; +import type express from "express"; +import { Source, sourceUri } from "../env.js"; + +const rumbaProxy = httpProxy.createProxy({ + target: sourceUri(Source.rumba), + changeOrigin: true, + autoRewrite: true, +}); + +export function rumba(req: express.Request, res: express.Response) { + rumbaProxy.web(req, res); +} diff --git a/gcp/function/src/handlers/spa.ts b/gcp/function/src/handlers/spa.ts new file mode 100644 index 000000000000..2cfb1db68cf1 --- /dev/null +++ b/gcp/function/src/handlers/spa.ts @@ -0,0 +1,16 @@ +import type express from "express"; +import * as path from "node:path"; +import { slugToFolder } from "@yari-internal/slug-utils"; +import { withResponseHeaders } from "../headers.js"; + +export async function spa(req: express.Request, res: express.Response) { + const rPath = req.path; + const folderName = slugToFolder(rPath); + let filePath = path.join("/tmp/bar/", folderName); + if (path.extname(filePath) === "") { + filePath = path.join(filePath, "index.html"); + } + return withResponseHeaders(res, { csp: true, xFrame: true }).sendFile( + filePath + ); +} diff --git a/gcp/function/src/headers.ts b/gcp/function/src/headers.ts new file mode 100644 index 000000000000..be3713c04cf4 --- /dev/null +++ b/gcp/function/src/headers.ts @@ -0,0 +1,23 @@ +import type express from "express"; +import { CSP_VALUE } from "@yari-internal/constants"; +import { RUNTIME_ENV } from "./env.js"; + +export function withResponseHeaders( + res: express.Response, + { csp = false, xFrame = false }: { csp?: boolean; xFrame?: boolean } = {} +): express.Response { + [ + ["X-XSS-Protection", "1; mode=block"], + ["X-Content-Type-Options", "nosniff"], + ["Strict-Transport-Security", "max-age=63072000"], + ...(csp && RUNTIME_ENV !== "local" + ? [["Content-Security-Policy", CSP_VALUE]] + : []), + ...(xFrame ? [["X-Frame-Options", "DENY"]] : []), + ].forEach(([k, v]) => k && v && res.append(k, v)); + return res; +} + +export function country(res: express.Request): string { + return res.header("X-Appengine-Country") || ""; +} diff --git a/gcp/function/src/index.ts b/gcp/function/src/index.ts new file mode 100644 index 000000000000..4ba3c5a69e92 --- /dev/null +++ b/gcp/function/src/index.ts @@ -0,0 +1,4 @@ +import { handler } from "./app.js"; +import functions from "@google-cloud/functions-framework"; + +functions.http("mdnHandler", handler); diff --git a/gcp/function/src/source.ts b/gcp/function/src/source.ts new file mode 100644 index 000000000000..ec0170f174e0 --- /dev/null +++ b/gcp/function/src/source.ts @@ -0,0 +1,16 @@ +import type express from "express"; +import { Source, sourceUri } from "./env.js"; + +export interface Transform { + source: Source; + http: (source: string) => express.Handler; + file: (source: string) => express.Handler; +} + +export function responder(transform: Transform): express.Handler { + const source = sourceUri(transform.source); + if (source.startsWith("http://") || source.startsWith("https://")) { + return transform.http(source); + } + return transform.file(source); +} diff --git a/gcp/function/tsconfig.json b/gcp/function/tsconfig.json new file mode 100644 index 000000000000..69267dcb0dbe --- /dev/null +++ b/gcp/function/tsconfig.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Node 18 + ESM + Strictest", + "compilerOptions": { + "lib": [ + "es2022" + ], + "module": "es2022", + "target": "es2022", + "strict": true, + "esModuleInterop": false, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "nodenext", + "allowUnusedLabels": false, + "allowUnreachableCode": false, + "exactOptionalPropertyTypes": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "importsNotUsedAsValues": "error", + "checkJs": true + }, + "ts-node": { + "esm": true, + "swc": true + }, + "exclude": ["src/internal/*"] +} From 70884a5226da79c4a4b931ec18fccceadc38d1a9 Mon Sep 17 00:00:00 2001 From: Florian Merz Date: Fri, 3 Mar 2023 20:10:09 +0100 Subject: [PATCH 002/343] more --- gcp/function/package.json | 12 ++++++++---- gcp/function/src/app.ts | 14 +++++++++----- gcp/function/src/handlers/bcdApi.ts | 1 + gcp/function/src/handlers/client.ts | 5 ++++- gcp/function/src/handlers/content.ts | 14 +++++++++++++- gcp/function/src/index.ts | 6 +++++- gcp/function/tsconfig.json | 4 +--- 7 files changed, 41 insertions(+), 15 deletions(-) diff --git a/gcp/function/package.json b/gcp/function/package.json index 9deeb9772370..0131e9b12e99 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -1,21 +1,25 @@ { "name": "mdn-function", "version": "0.0.1", + "private": true, "description": "", - "main": "src/index.js", + "license": "MPL-2.0", + "author": "", "type": "module", + "main": "src/index.js", "scripts": {}, - "author": "", - "license": "MPL-2.0", "dependencies": { + "@google-cloud/functions-framework": "^3.1.3", "@yari-internal/constants": "file:src/internal/libs/constants", "@yari-internal/fundamental-redirects": "file:src/internals/fundamental-redirects", "@yari-internal/locale-utils": "file:src/internal/libs/locale-utils", "@yari-internal/slug-utils": "file:src/internal/libs/slug-utils", - "@google-cloud/functions-framework": "^3.1.3", "dotenv": "^16.0.3", "express": "^4.18.2", "http-proxy": "^1.18.1", "sanitize-filename": "^1.6.3" + }, + "engines": { + "node": ">=18.0.0" } } diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 4ca59e2fa12f..1afc45060926 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -10,20 +10,24 @@ import { spa } from "./handlers/spa.js"; import { rumba } from "./handlers/rumba.js"; const mainRouter = Router(); +const docsHandler = docs(); mainRouter.get("/bcd/api/*", bcdApi()); mainRouter.all("/api/*", rumba); mainRouter.all("/users/fxa/*", rumba); mainRouter.get("/[^/]+/plus/*", spa); -mainRouter.get("/[^/]+/docs/*", docs()); +mainRouter.get("/[^/]+/docs/*", docsHandler); +mainRouter.get("/[^/]+/search-index.json", docsHandler); mainRouter.get("*", client()); -export async function handler(req: express.Request, res: express.Response) { +export async function handler( + req: express.Request, + res: express.Response, + next: express.NextFunction +) { const rPath = req.path; const reqOrigin = origin(req); if (reqOrigin === Origin.main && !rPath.includes("/_sample_.")) { - return mainRouter(req, res, () => { - console.error("must not be called"); - }); + return mainRouter(req, res, next); } else if (reqOrigin === Origin.liveSamples) { return liveSamples(req, res); } else { diff --git a/gcp/function/src/handlers/bcdApi.ts b/gcp/function/src/handlers/bcdApi.ts index 177543e8e541..59bc4b4ba918 100644 --- a/gcp/function/src/handlers/bcdApi.ts +++ b/gcp/function/src/handlers/bcdApi.ts @@ -9,6 +9,7 @@ export function bcdApi(): express.Handler { source: Source.bcdApi, http(source) { const bcdProxy = httpProxy.createProxy({ + prependPath: true, changeOrigin: true, target: source, autoRewrite: true, diff --git a/gcp/function/src/handlers/client.ts b/gcp/function/src/handlers/client.ts index e2cc59d7b0e2..cb3becae98f5 100644 --- a/gcp/function/src/handlers/client.ts +++ b/gcp/function/src/handlers/client.ts @@ -9,11 +9,14 @@ export function client(): express.Handler { source: Source.client, http(source) { const clientProxy = httpProxy.createProxy({ + prependPath: true, changeOrigin: true, target: source, autoRewrite: true, }); - return (req, res) => clientProxy.web(req, res); + return (req, res) => { + clientProxy.web(req, res); + }; }, file(source) { return (req, res) => { diff --git a/gcp/function/src/handlers/content.ts b/gcp/function/src/handlers/content.ts index 45942041efc4..6afb3cfa8539 100644 --- a/gcp/function/src/handlers/content.ts +++ b/gcp/function/src/handlers/content.ts @@ -11,11 +11,23 @@ export function docs(): express.Handler { source: Source.content, http(source) { const contentProxy = httpProxy.createProxy({ + prependPath: true, + ignorePath: true, changeOrigin: true, target: source, autoRewrite: true, }); - return (req, res) => contentProxy.web(req, res); + contentProxy.on("proxyReq", (proxyReq, req) => { + const rPath = req.url; + let folderName = slugToFolder(rPath); + if (path.extname(folderName) === "") { + folderName = path.join(folderName, "index.html"); + } + proxyReq.path = path.join(proxyReq.path, folderName); + }); + return (req, res) => { + contentProxy.web(req, res); + }; }, file(source) { return (req, res) => { diff --git a/gcp/function/src/index.ts b/gcp/function/src/index.ts index 4ba3c5a69e92..af4394a37e76 100644 --- a/gcp/function/src/index.ts +++ b/gcp/function/src/index.ts @@ -1,4 +1,8 @@ import { handler } from "./app.js"; import functions from "@google-cloud/functions-framework"; -functions.http("mdnHandler", handler); +functions.http("mdnHandler", (req, res) => + handler(req, res, () => { + /* noop */ + }) +); diff --git a/gcp/function/tsconfig.json b/gcp/function/tsconfig.json index 69267dcb0dbe..3e1c5270f1ea 100644 --- a/gcp/function/tsconfig.json +++ b/gcp/function/tsconfig.json @@ -2,9 +2,7 @@ "$schema": "https://json.schemastore.org/tsconfig", "display": "Node 18 + ESM + Strictest", "compilerOptions": { - "lib": [ - "es2022" - ], + "lib": ["es2022"], "module": "es2022", "target": "es2022", "strict": true, From cccd3b8fe07f652af6752585ef3f577cceb7ea9a Mon Sep 17 00:00:00 2001 From: Florian Merz Date: Mon, 6 Mar 2023 14:31:43 +0100 Subject: [PATCH 003/343] package --- gcp/function/.gcloudignore | 4 +++- gcp/function/.gitignore | 2 +- gcp/function/package.json | 7 ++++++- gcp/function/src/handlers/content.ts | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/gcp/function/.gcloudignore b/gcp/function/.gcloudignore index 5a616cba337e..e481753cb246 100644 --- a/gcp/function/.gcloudignore +++ b/gcp/function/.gcloudignore @@ -14,4 +14,6 @@ .gitignore node_modules -#!include:.gitignore +src/**/*.ts +!#include:.gitignore + diff --git a/gcp/function/.gitignore b/gcp/function/.gitignore index 7086d62cf2a9..b3397798ee69 100644 --- a/gcp/function/.gitignore +++ b/gcp/function/.gitignore @@ -1,3 +1,3 @@ -.env +.env* src/**/*.js src/internal diff --git a/gcp/function/package.json b/gcp/function/package.json index 0131e9b12e99..9cd537a8c209 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -7,7 +7,12 @@ "author": "", "type": "module", "main": "src/index.js", - "scripts": {}, + "scripts": { + "build": "tsc -b", + "clean": "tsc -b --clean", + "deploy": "gcloud functions deploy mdn-two --region europe-west3 --runtime nodejs18 --trigger-http --allow-unauthenticated", + "package": "zip -r -X f.zip . -i package.json .env 'src/*.js' 'src/handlers/*.js' 'src/internal'" + }, "dependencies": { "@google-cloud/functions-framework": "^3.1.3", "@yari-internal/constants": "file:src/internal/libs/constants", diff --git a/gcp/function/src/handlers/content.ts b/gcp/function/src/handlers/content.ts index 6afb3cfa8539..af3e2e160567 100644 --- a/gcp/function/src/handlers/content.ts +++ b/gcp/function/src/handlers/content.ts @@ -19,7 +19,7 @@ export function docs(): express.Handler { }); contentProxy.on("proxyReq", (proxyReq, req) => { const rPath = req.url; - let folderName = slugToFolder(rPath); + let folderName = slugToFolder(rPath || ""); if (path.extname(folderName) === "") { folderName = path.join(folderName, "index.html"); } From d99a296b419c9c21b1a35b3913d33a919a78f947 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 6 Mar 2023 16:41:00 +0100 Subject: [PATCH 004/343] chore(gcp/function): create link in prepare script --- gcp/function/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gcp/function/package.json b/gcp/function/package.json index 9cd537a8c209..605d6097da26 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -11,7 +11,8 @@ "build": "tsc -b", "clean": "tsc -b --clean", "deploy": "gcloud functions deploy mdn-two --region europe-west3 --runtime nodejs18 --trigger-http --allow-unauthenticated", - "package": "zip -r -X f.zip . -i package.json .env 'src/*.js' 'src/handlers/*.js' 'src/internal'" + "package": "zip -r -X f.zip . -i package.json .env 'src/*.js' 'src/handlers/*.js' 'src/internal'", + "prepare": "ln -s ../../libs ./src/internal" }, "dependencies": { "@google-cloud/functions-framework": "^3.1.3", From f030cbc6f1461aefcb395ba61a3e95d0239f7a4d Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 6 Mar 2023 16:50:01 +0100 Subject: [PATCH 005/343] docs(gcp): add README --- gcp/README.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 gcp/README.md diff --git a/gcp/README.md b/gcp/README.md new file mode 100644 index 000000000000..054d7c1de898 --- /dev/null +++ b/gcp/README.md @@ -0,0 +1,5 @@ +## Setup + +1. Install *google-cloud-sdk* (e.g. `brew install google-cloud-sdk`). +2. Run `gcloud auth login`. +3. Run `gcloud config set project `. From 86cc4288427d54f65f784c7087a44a700118de09 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 6 Mar 2023 17:56:24 +0100 Subject: [PATCH 006/343] fixup! chore(gcp/function): create link in prepare script --- gcp/function/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/function/package.json b/gcp/function/package.json index 605d6097da26..851438a8d786 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -12,7 +12,7 @@ "clean": "tsc -b --clean", "deploy": "gcloud functions deploy mdn-two --region europe-west3 --runtime nodejs18 --trigger-http --allow-unauthenticated", "package": "zip -r -X f.zip . -i package.json .env 'src/*.js' 'src/handlers/*.js' 'src/internal'", - "prepare": "ln -s ../../libs ./src/internal" + "prepare": "rm -f ./src/internal && ln -s ../../../libs ./src/internal" }, "dependencies": { "@google-cloud/functions-framework": "^3.1.3", From f6e762cec9d66d14a8dee2ba8bdcf29a83443620 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 6 Mar 2023 17:57:47 +0100 Subject: [PATCH 007/343] fix(gcp/function): add typescript to devDependencies --- gcp/function/package-lock.json | 19 +++++++++++++++++++ gcp/function/package.json | 3 +++ 2 files changed, 22 insertions(+) diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index e38b120d4f18..66656841ce8c 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -18,6 +18,12 @@ "express": "^4.18.2", "http-proxy": "^1.18.1", "sanitize-filename": "^1.6.3" + }, + "devDependencies": { + "typescript": "^4.9.5" + }, + "engines": { + "node": ">=18.0.0" } }, "../../../libs/constants": { @@ -1360,6 +1366,19 @@ "node": ">= 0.6" } }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/gcp/function/package.json b/gcp/function/package.json index 851438a8d786..13748492ab55 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -27,5 +27,8 @@ }, "engines": { "node": ">=18.0.0" + }, + "devDependencies": { + "typescript": "^4.9.5" } } From 8d42cb22b380a8401ed206bdcb563ec41420e26d Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 6 Mar 2023 18:00:01 +0100 Subject: [PATCH 008/343] fix(gcp/function): correct internal paths --- gcp/function/package-lock.json | 42 ++++++++++++++++++++++++++-------- gcp/function/package.json | 8 +++---- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index 66656841ce8c..2d647b744f2a 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -10,10 +10,10 @@ "license": "MPL-2.0", "dependencies": { "@google-cloud/functions-framework": "^3.1.3", - "@yari-internal/constants": "file:src/internal/libs/constants", - "@yari-internal/fundamental-redirects": "file:src/internals/fundamental-redirects", - "@yari-internal/locale-utils": "file:src/internal/libs/locale-utils", - "@yari-internal/slug-utils": "file:src/internal/libs/slug-utils", + "@yari-internal/constants": "file:src/internal/constants", + "@yari-internal/fundamental-redirects": "file:src/internal/fundamental-redirects", + "@yari-internal/locale-utils": "file:src/internal/locale-utils", + "@yari-internal/slug-utils": "file:src/internal/slug-utils", "dotenv": "^16.0.3", "express": "^4.18.2", "http-proxy": "^1.18.1", @@ -204,19 +204,19 @@ } }, "node_modules/@yari-internal/constants": { - "resolved": "src/internal/libs/constants", + "resolved": "src/internal/constants", "link": true }, "node_modules/@yari-internal/fundamental-redirects": { - "resolved": "src/internals/fundamental-redirects", + "resolved": "src/internal/fundamental-redirects", "link": true }, "node_modules/@yari-internal/locale-utils": { - "resolved": "src/internal/libs/locale-utils", + "resolved": "src/internal/locale-utils", "link": true }, "node_modules/@yari-internal/slug-utils": { - "resolved": "src/internal/libs/slug-utils", + "resolved": "src/internal/slug-utils", "link": true }, "node_modules/accepts": { @@ -1469,21 +1469,45 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "src/internal/constants": { + "version": "0.0.1", + "license": "MPL-2.0" + }, + "src/internal/fundamental-redirects": { + "version": "0.0.1", + "license": "MPL-2.0" + }, "src/internal/libs/constants": { "version": "0.0.1", + "extraneous": true, "license": "MPL-2.0" }, "src/internal/libs/locale-utils": { "version": "0.0.1", + "extraneous": true, "license": "MPL-2.0" }, "src/internal/libs/slug-utils": { "version": "0.0.1", + "extraneous": true, "license": "MPL-2.0", "dependencies": { "sanitize-filename": "^1.6.3" } }, - "src/internals/fundamental-redirects": {} + "src/internal/locale-utils": { + "version": "0.0.1", + "license": "MPL-2.0" + }, + "src/internal/slug-utils": { + "version": "0.0.1", + "license": "MPL-2.0", + "dependencies": { + "sanitize-filename": "^1.6.3" + } + }, + "src/internals/fundamental-redirects": { + "extraneous": true + } } } diff --git a/gcp/function/package.json b/gcp/function/package.json index 13748492ab55..ee573a826bcb 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -16,10 +16,10 @@ }, "dependencies": { "@google-cloud/functions-framework": "^3.1.3", - "@yari-internal/constants": "file:src/internal/libs/constants", - "@yari-internal/fundamental-redirects": "file:src/internals/fundamental-redirects", - "@yari-internal/locale-utils": "file:src/internal/libs/locale-utils", - "@yari-internal/slug-utils": "file:src/internal/libs/slug-utils", + "@yari-internal/constants": "file:src/internal/constants", + "@yari-internal/fundamental-redirects": "file:src/internal/fundamental-redirects", + "@yari-internal/locale-utils": "file:src/internal/locale-utils", + "@yari-internal/slug-utils": "file:src/internal/slug-utils", "dotenv": "^16.0.3", "express": "^4.18.2", "http-proxy": "^1.18.1", From 08b951ae8e91c679a42ce9f3c6f0bb363b15a4af Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 6 Mar 2023 18:20:36 +0100 Subject: [PATCH 009/343] fix(gcp/functions): copy as part of build --- gcp/function/package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gcp/function/package.json b/gcp/function/package.json index ee573a826bcb..e1ce4c2f54fb 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -8,11 +8,10 @@ "type": "module", "main": "src/index.js", "scripts": { - "build": "tsc -b", + "build": "tsc -b && rm -rf ./src/internal && mkdir ./src/internal && cp -R ../../libs/{constants,fundamental-redirects,locale-utils,slug-utils} ./src/internal/", "clean": "tsc -b --clean", "deploy": "gcloud functions deploy mdn-two --region europe-west3 --runtime nodejs18 --trigger-http --allow-unauthenticated", - "package": "zip -r -X f.zip . -i package.json .env 'src/*.js' 'src/handlers/*.js' 'src/internal'", - "prepare": "rm -f ./src/internal && ln -s ../../../libs ./src/internal" + "package": "zip -r -X f.zip . -i package.json .env 'src/*.js' 'src/handlers/*.js' 'src/internal'" }, "dependencies": { "@google-cloud/functions-framework": "^3.1.3", From 2112e1f5e0bdc562f0f1df0a4a38a0b740d67138 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 6 Mar 2023 18:36:07 +0100 Subject: [PATCH 010/343] feat(gcp/functions): proxy interactive-examples --- gcp/function/src/env.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gcp/function/src/env.ts b/gcp/function/src/env.ts index 68cae3e0821e..3dc7b028db3b 100644 --- a/gcp/function/src/env.ts +++ b/gcp/function/src/env.ts @@ -65,6 +65,9 @@ export const SOURCE_BCD_API: string = process.env["SOURCE_BCD_API"] || "https://developer.mozilla.org"; export const SOURCE_CLIENT: string = process.env["SOURCE_CLIENT"] || "https://developer.mozilla.org"; +export const SOURCE_INTERACTIVE_SAMPLES: string = + process.env["SOURCE_INTERACTIVE_SAMPLES"] || + "https://interactive-examples.mdn.mozilla.net"; export const SOURCE_RUMBA: string = process.env["SOURCE_RUMBA"] || "https://developer.mozilla.org"; @@ -76,6 +79,8 @@ export function sourceUri(source: Source): string { return SOURCE_BCD_API; case Source.client: return SOURCE_CLIENT; + case Source.interactiveSamples: + return SOURCE_INTERACTIVE_SAMPLES; case Source.liveSamples: return SOURCE_LIVE_SAMPLES; case Source.rumba: From 05e271418033e15f34733441299cd42a4fc1b4a0 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 6 Mar 2023 19:13:33 +0100 Subject: [PATCH 011/343] fix(gcp/function): swap interactive/live samples --- gcp/function/src/env.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gcp/function/src/env.ts b/gcp/function/src/env.ts index 3dc7b028db3b..7fb04aee5355 100644 --- a/gcp/function/src/env.ts +++ b/gcp/function/src/env.ts @@ -35,10 +35,10 @@ export const RUNTIME_ENV: string = process.env["RUNTIME_ENV"] || "prod"; export const ORIGIN_MAIN: string = process.env["ORIGIN_MAIN"] || "developer.mozilla.org"; export const ORIGIN_LIVE_SAMPLES: string = - process.env["ORIGIN_LIVE_SAMPLES"] || "interactive-examples.mdn.mozilla.net"; + process.env["ORIGIN_LIVE_SAMPLES"] || "yari-demos.prod.mdn.mozit.cloud"; export const ORIGIN_INTERACTIVE_SAMPLES: string = process.env["ORIGIN_INTERACTIVE_SAMPLES"] || - "yari-demos.prod.mdn.mozit.cloud"; + "interactive-examples.mdn.mozilla.net"; export function origin(req: express.Request): Origin { switch (req.hostname) { From 1a2fc66e47b368fef5b60cfb20f2bbd0634516cf Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 6 Mar 2023 19:31:02 +0100 Subject: [PATCH 012/343] feat(gcp/function): add start script via ts-node --- gcp/function/package-lock.json | 168 +++++++++++++++++++++++++++++++++ gcp/function/package.json | 4 +- 2 files changed, 171 insertions(+), 1 deletion(-) diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index 2d647b744f2a..3e0cd4eb7c2d 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -20,6 +20,7 @@ "sanitize-filename": "^1.6.3" }, "devDependencies": { + "ts-node": "^10.9.1", "typescript": "^4.9.5" }, "engines": { @@ -109,6 +110,18 @@ "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, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@google-cloud/functions-framework": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@google-cloud/functions-framework/-/functions-framework-3.1.3.tgz", @@ -131,6 +144,55 @@ "node": ">=10.0.0" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "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, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "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 + }, + "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 + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -231,6 +293,27 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", @@ -273,6 +356,12 @@ "node": ">=4" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -405,6 +494,12 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "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 + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -430,6 +525,15 @@ "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, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dotenv": { "version": "16.0.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", @@ -849,6 +953,12 @@ "node": ">=10" } }, + "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 + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1346,6 +1456,49 @@ "utf8-byte-length": "^1.0.1" } }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "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": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", @@ -1428,6 +1581,12 @@ "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 + }, "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", @@ -1469,6 +1628,15 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "src/internal/constants": { "version": "0.0.1", "license": "MPL-2.0" diff --git a/gcp/function/package.json b/gcp/function/package.json index e1ce4c2f54fb..b5a93f31e35d 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -11,7 +11,8 @@ "build": "tsc -b && rm -rf ./src/internal && mkdir ./src/internal && cp -R ../../libs/{constants,fundamental-redirects,locale-utils,slug-utils} ./src/internal/", "clean": "tsc -b --clean", "deploy": "gcloud functions deploy mdn-two --region europe-west3 --runtime nodejs18 --trigger-http --allow-unauthenticated", - "package": "zip -r -X f.zip . -i package.json .env 'src/*.js' 'src/handlers/*.js' 'src/internal'" + "package": "zip -r -X f.zip . -i package.json .env 'src/*.js' 'src/handlers/*.js' 'src/internal'", + "start": "ts-node src/cli.ts" }, "dependencies": { "@google-cloud/functions-framework": "^3.1.3", @@ -28,6 +29,7 @@ "node": ">=18.0.0" }, "devDependencies": { + "ts-node": "^10.9.1", "typescript": "^4.9.5" } } From 4008605d2f8eeac71ae9537a298661eb9dea3baf Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 6 Mar 2023 19:54:53 +0100 Subject: [PATCH 013/343] feat(gcp/function): proxy interactive-examples --- gcp/function/src/app.ts | 3 +++ gcp/function/src/handlers/interactiveSamples.ts | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 gcp/function/src/handlers/interactiveSamples.ts diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 1afc45060926..ad544c06e21e 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -4,6 +4,7 @@ import { client } from "./handlers/client.js"; import type express from "express"; import { Router } from "express"; import { Origin, origin } from "./env.js"; +import { interactiveSamples } from "./handlers/interactiveSamples.js"; import { liveSamples } from "./handlers/liveSamples.js"; import { bcdApi } from "./handlers/bcdApi.js"; import { spa } from "./handlers/spa.js"; @@ -28,6 +29,8 @@ export async function handler( const reqOrigin = origin(req); if (reqOrigin === Origin.main && !rPath.includes("/_sample_.")) { return mainRouter(req, res, next); + } else if (reqOrigin === Origin.interactiveSamples) { + return interactiveSamples(req, res); } else if (reqOrigin === Origin.liveSamples) { return liveSamples(req, res); } else { diff --git a/gcp/function/src/handlers/interactiveSamples.ts b/gcp/function/src/handlers/interactiveSamples.ts new file mode 100644 index 000000000000..960a977e28a8 --- /dev/null +++ b/gcp/function/src/handlers/interactiveSamples.ts @@ -0,0 +1,17 @@ +import type express from "express"; +import { SOURCE_INTERACTIVE_SAMPLES } from "../env.js"; +import httpProxy from "http-proxy"; + +const proxy = httpProxy.createProxy({ + prependPath: true, + changeOrigin: true, + target: SOURCE_INTERACTIVE_SAMPLES, + autoRewrite: true, +}); + +export async function interactiveSamples( + req: express.Request, + res: express.Response +) { + proxy.web(req, res); +} From 8a191714ae5e726096846ba87c495dd3bd9dc832 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 6 Mar 2023 19:55:59 +0100 Subject: [PATCH 014/343] fix(gcp/function): use headers.host over hostname Allows using "localhost:5042" as origin locally. --- gcp/function/src/env.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gcp/function/src/env.ts b/gcp/function/src/env.ts index 7fb04aee5355..873f4619c0b5 100644 --- a/gcp/function/src/env.ts +++ b/gcp/function/src/env.ts @@ -41,7 +41,8 @@ export const ORIGIN_INTERACTIVE_SAMPLES: string = "interactive-examples.mdn.mozilla.net"; export function origin(req: express.Request): Origin { - switch (req.hostname) { + const host = req.headers.host || req.hostname; + switch (host) { case ORIGIN_MAIN: return Origin.main; case ORIGIN_LIVE_SAMPLES: From b05446ea5fa82ffb5a6a613d28ad955f11d3d769 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 6 Mar 2023 22:22:55 +0100 Subject: [PATCH 015/343] style(gcp): run prettier -w --- gcp/README.md | 5 ++--- gcp/function/package.json | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/gcp/README.md b/gcp/README.md index 054d7c1de898..c643c7c5eeb5 100644 --- a/gcp/README.md +++ b/gcp/README.md @@ -1,5 +1,4 @@ ## Setup -1. Install *google-cloud-sdk* (e.g. `brew install google-cloud-sdk`). -2. Run `gcloud auth login`. -3. Run `gcloud config set project `. +1. Install _google-cloud-sdk_ (e.g. `brew install google-cloud-sdk`). +2. Run `gcloud auth login`. 3. Run `gcloud config set project `. diff --git a/gcp/function/package.json b/gcp/function/package.json index b5a93f31e35d..25d1ff21ceec 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -25,11 +25,11 @@ "http-proxy": "^1.18.1", "sanitize-filename": "^1.6.3" }, - "engines": { - "node": ">=18.0.0" - }, "devDependencies": { "ts-node": "^10.9.1", "typescript": "^4.9.5" + }, + "engines": { + "node": ">=18.0.0" } } From 98b5ff5e9969d9ec0b0407608e4b43360f6f6d1b Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 6 Mar 2023 23:00:48 +0100 Subject: [PATCH 016/343] Revert "feat(gcp/function): proxy interactive-examples" This reverts commit 4008605d2f8eeac71ae9537a298661eb9dea3baf. --- gcp/function/src/app.ts | 3 --- gcp/function/src/handlers/interactiveSamples.ts | 17 ----------------- 2 files changed, 20 deletions(-) delete mode 100644 gcp/function/src/handlers/interactiveSamples.ts diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index ad544c06e21e..1afc45060926 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -4,7 +4,6 @@ import { client } from "./handlers/client.js"; import type express from "express"; import { Router } from "express"; import { Origin, origin } from "./env.js"; -import { interactiveSamples } from "./handlers/interactiveSamples.js"; import { liveSamples } from "./handlers/liveSamples.js"; import { bcdApi } from "./handlers/bcdApi.js"; import { spa } from "./handlers/spa.js"; @@ -29,8 +28,6 @@ export async function handler( const reqOrigin = origin(req); if (reqOrigin === Origin.main && !rPath.includes("/_sample_.")) { return mainRouter(req, res, next); - } else if (reqOrigin === Origin.interactiveSamples) { - return interactiveSamples(req, res); } else if (reqOrigin === Origin.liveSamples) { return liveSamples(req, res); } else { diff --git a/gcp/function/src/handlers/interactiveSamples.ts b/gcp/function/src/handlers/interactiveSamples.ts deleted file mode 100644 index 960a977e28a8..000000000000 --- a/gcp/function/src/handlers/interactiveSamples.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type express from "express"; -import { SOURCE_INTERACTIVE_SAMPLES } from "../env.js"; -import httpProxy from "http-proxy"; - -const proxy = httpProxy.createProxy({ - prependPath: true, - changeOrigin: true, - target: SOURCE_INTERACTIVE_SAMPLES, - autoRewrite: true, -}); - -export async function interactiveSamples( - req: express.Request, - res: express.Response -) { - proxy.web(req, res); -} From 299fb87b4f1f2eabc995138f85add17255fc7af5 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 6 Mar 2023 23:54:23 +0100 Subject: [PATCH 017/343] fix(gcp/functions): handle liveSamples properly --- gcp/function/src/app.ts | 7 +++++-- gcp/function/src/handlers/liveSamples.ts | 10 ---------- 2 files changed, 5 insertions(+), 12 deletions(-) delete mode 100644 gcp/function/src/handlers/liveSamples.ts diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 1afc45060926..254bfc7c28cc 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -4,7 +4,6 @@ import { client } from "./handlers/client.js"; import type express from "express"; import { Router } from "express"; import { Origin, origin } from "./env.js"; -import { liveSamples } from "./handlers/liveSamples.js"; import { bcdApi } from "./handlers/bcdApi.js"; import { spa } from "./handlers/spa.js"; import { rumba } from "./handlers/rumba.js"; @@ -19,6 +18,10 @@ mainRouter.get("/[^/]+/docs/*", docsHandler); mainRouter.get("/[^/]+/search-index.json", docsHandler); mainRouter.get("*", client()); +const liveSampleRouter = Router(); +liveSampleRouter.get("/[^/]+/docs/*/_sample_.*.html", client()); +liveSampleRouter.get("/[^/]+/docs/*/*.(png|jpeg|jpg|gif|svg|webp)", client()); + export async function handler( req: express.Request, res: express.Response, @@ -29,7 +32,7 @@ export async function handler( if (reqOrigin === Origin.main && !rPath.includes("/_sample_.")) { return mainRouter(req, res, next); } else if (reqOrigin === Origin.liveSamples) { - return liveSamples(req, res); + return liveSampleRouter(req, res, next); } else { return res.status(404).send(); } diff --git a/gcp/function/src/handlers/liveSamples.ts b/gcp/function/src/handlers/liveSamples.ts deleted file mode 100644 index 9634841d4f04..000000000000 --- a/gcp/function/src/handlers/liveSamples.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type express from "express"; -import * as path from "node:path"; -import { slugToFolder } from "@yari-internal/slug-utils"; - -export async function liveSamples(req: express.Request, res: express.Response) { - const rPath = req.path; - const folderName = slugToFolder(rPath); - const filePath = path.join("/tmp/231/", folderName); - res.sendFile(filePath); -} From 262dfe7cf56028b339bd5ee35e6d37c434bf6ea6 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 7 Mar 2023 00:30:59 +0100 Subject: [PATCH 018/343] fix(gcp/function): lowercase liveSample pathname --- gcp/function/src/app.ts | 7 ++++--- gcp/function/src/middlewares/pathnameLC.ts | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 gcp/function/src/middlewares/pathnameLC.ts diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 254bfc7c28cc..f2f34895b229 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -1,12 +1,12 @@ -import { docs } from "./handlers/content.js"; -import { client } from "./handlers/client.js"; - import type express from "express"; import { Router } from "express"; import { Origin, origin } from "./env.js"; +import { docs } from "./handlers/content.js"; +import { client } from "./handlers/client.js"; import { bcdApi } from "./handlers/bcdApi.js"; import { spa } from "./handlers/spa.js"; import { rumba } from "./handlers/rumba.js"; +import { pathnameLC } from "./middlewares/pathnameLC.js"; const mainRouter = Router(); const docsHandler = docs(); @@ -19,6 +19,7 @@ mainRouter.get("/[^/]+/search-index.json", docsHandler); mainRouter.get("*", client()); const liveSampleRouter = Router(); +liveSampleRouter.use(pathnameLC); liveSampleRouter.get("/[^/]+/docs/*/_sample_.*.html", client()); liveSampleRouter.get("/[^/]+/docs/*/*.(png|jpeg|jpg|gif|svg|webp)", client()); diff --git a/gcp/function/src/middlewares/pathnameLC.ts b/gcp/function/src/middlewares/pathnameLC.ts new file mode 100644 index 000000000000..839b4c3e91eb --- /dev/null +++ b/gcp/function/src/middlewares/pathnameLC.ts @@ -0,0 +1,17 @@ +import * as url from "node:url"; + +import type express from "express"; + +export function pathnameLC( + req: express.Request, + _res: express.Response, + next: express.NextFunction +) { + console.log("before", req.url); + const urlParsed = url.parse(req.url); + if (urlParsed.pathname) { + urlParsed.pathname = urlParsed.pathname.toLowerCase(); + } + req.url = url.format(urlParsed); + next(); +} From 46fe4a461864adf1222ed6756173a5070f1e4bba Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 7 Mar 2023 00:35:58 +0100 Subject: [PATCH 019/343] fixup! fix(gcp/function): lowercase liveSample pathname --- gcp/function/src/app.ts | 3 +++ gcp/function/src/middlewares/pathnameLC.ts | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index f2f34895b229..4b37ae7e7788 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -22,6 +22,9 @@ const liveSampleRouter = Router(); liveSampleRouter.use(pathnameLC); liveSampleRouter.get("/[^/]+/docs/*/_sample_.*.html", client()); liveSampleRouter.get("/[^/]+/docs/*/*.(png|jpeg|jpg|gif|svg|webp)", client()); +liveSampleRouter.get("*", (_req: express.Request, res: express.Response) => + res.status(404).send() +); export async function handler( req: express.Request, diff --git a/gcp/function/src/middlewares/pathnameLC.ts b/gcp/function/src/middlewares/pathnameLC.ts index 839b4c3e91eb..ac3c6b625cd9 100644 --- a/gcp/function/src/middlewares/pathnameLC.ts +++ b/gcp/function/src/middlewares/pathnameLC.ts @@ -7,7 +7,6 @@ export function pathnameLC( _res: express.Response, next: express.NextFunction ) { - console.log("before", req.url); const urlParsed = url.parse(req.url); if (urlParsed.pathname) { urlParsed.pathname = urlParsed.pathname.toLowerCase(); From 818fa632b4816e8c8b659bdb30636421899774d2 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 7 Mar 2023 00:53:49 +0100 Subject: [PATCH 020/343] chore(gcp/function): add build-deploy-clean script --- gcp/function/package-lock.json | 758 +++++++++++++++++++++++++++++++++ gcp/function/package.json | 2 + 2 files changed, 760 insertions(+) diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index 3e0cd4eb7c2d..063527001819 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -20,6 +20,7 @@ "sanitize-filename": "^1.6.3" }, "devDependencies": { + "npm-run-all": "^4.1.5", "ts-node": "^10.9.1", "typescript": "^4.9.5" }, @@ -378,6 +379,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, "node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -401,6 +408,16 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -462,6 +479,12 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -500,6 +523,31 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -508,6 +556,22 @@ "ms": "2.0.0" } }, + "node_modules/define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -563,6 +627,84 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-abstract": { + "version": "1.21.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.1.tgz", + "integrity": "sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.4", + "is-array-buffer": "^3.0.1", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -712,6 +854,33 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-intrinsic": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", @@ -725,6 +894,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -736,6 +936,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -747,6 +953,15 @@ "node": ">= 0.4.0" } }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -755,6 +970,30 @@ "node": ">=4" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", @@ -829,6 +1068,20 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -852,11 +1105,53 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -879,6 +1174,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-generator-function": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", @@ -893,6 +1203,91 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-typed-array": { "version": "1.1.10", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", @@ -911,11 +1306,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -931,6 +1350,34 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -967,6 +1414,15 @@ "node": ">= 0.6" } }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -1010,6 +1466,18 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -1031,6 +1499,12 @@ "node": ">= 0.6" } }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -1050,6 +1524,45 @@ "semver": "bin/semver" } }, + "node_modules/npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm-run-all/node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -1058,6 +1571,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -1135,6 +1675,15 @@ "node": ">=8" } }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -1145,6 +1694,39 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -1247,6 +1829,23 @@ "node": ">=8" } }, + "node_modules/regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1295,6 +1894,20 @@ } ] }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -1369,6 +1982,36 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shell-quote": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.0.tgz", + "integrity": "sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -1418,6 +2061,60 @@ "node": ">= 0.8" } }, + "node_modules/string.prototype.padend": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.4.tgz", + "integrity": "sha512-67otBXoksdjsnXXRUq+KMVTdlVRZ2af422Y0aTyTjVaoQkGr3mxl2Bc5emi7dOQ3OGVVQQskmLEWwFXwommpNw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1519,6 +2216,20 @@ "node": ">= 0.6" } }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", @@ -1532,6 +2243,21 @@ "node": ">=4.2.0" } }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1604,6 +2330,34 @@ "node": ">= 0.8" } }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/which-typed-array": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", @@ -1638,10 +2392,12 @@ } }, "src/internal/constants": { + "name": "@yari-internal/constants", "version": "0.0.1", "license": "MPL-2.0" }, "src/internal/fundamental-redirects": { + "name": "@yari-internal/fundamental-redirects", "version": "0.0.1", "license": "MPL-2.0" }, @@ -1664,10 +2420,12 @@ } }, "src/internal/locale-utils": { + "name": "@yari-internal/locale-utils", "version": "0.0.1", "license": "MPL-2.0" }, "src/internal/slug-utils": { + "name": "@yari-internal/slug-utils", "version": "0.0.1", "license": "MPL-2.0", "dependencies": { diff --git a/gcp/function/package.json b/gcp/function/package.json index 25d1ff21ceec..77131ecede68 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -9,6 +9,7 @@ "main": "src/index.js", "scripts": { "build": "tsc -b && rm -rf ./src/internal && mkdir ./src/internal && cp -R ../../libs/{constants,fundamental-redirects,locale-utils,slug-utils} ./src/internal/", + "build-deploy-clean": "npm-run-all -s build deploy clean", "clean": "tsc -b --clean", "deploy": "gcloud functions deploy mdn-two --region europe-west3 --runtime nodejs18 --trigger-http --allow-unauthenticated", "package": "zip -r -X f.zip . -i package.json .env 'src/*.js' 'src/handlers/*.js' 'src/internal'", @@ -26,6 +27,7 @@ "sanitize-filename": "^1.6.3" }, "devDependencies": { + "npm-run-all": "^4.1.5", "ts-node": "^10.9.1", "typescript": "^4.9.5" }, From 825ab4f94060ae6d484876a96619a6b3af4a009d Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 7 Mar 2023 12:09:27 +0100 Subject: [PATCH 021/343] chore(gcp/function): extract liveSampleApp --- gcp/function/src/app.ts | 30 ++++++++++++++++-------------- gcp/function/src/cli.ts | 14 ++++++++++---- gcp/function/src/env.ts | 3 +-- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 4b37ae7e7788..192fb7a0e123 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -26,18 +26,20 @@ liveSampleRouter.get("*", (_req: express.Request, res: express.Response) => res.status(404).send() ); -export async function handler( - req: express.Request, - res: express.Response, - next: express.NextFunction -) { - const rPath = req.path; - const reqOrigin = origin(req); - if (reqOrigin === Origin.main && !rPath.includes("/_sample_.")) { - return mainRouter(req, res, next); - } else if (reqOrigin === Origin.liveSamples) { - return liveSampleRouter(req, res, next); - } else { - return res.status(404).send(); - } +export function createHandler(o?: Origin) { + return async ( + req: express.Request, + res: express.Response, + next: express.NextFunction + ) => { + const rPath = req.path; + const reqOrigin = o || origin(req); + if (reqOrigin === Origin.main && !rPath.includes("/_sample_.")) { + return mainRouter(req, res, next); + } else if (reqOrigin === Origin.liveSamples) { + return liveSampleRouter(req, res, next); + } else { + return res.status(404).send(); + } + }; } diff --git a/gcp/function/src/cli.ts b/gcp/function/src/cli.ts index 29ea0a6f572c..f0f1a7d06237 100644 --- a/gcp/function/src/cli.ts +++ b/gcp/function/src/cli.ts @@ -1,13 +1,19 @@ import express from "express"; -import { handler } from "./app.js"; +import { createHandler } from "./app.js"; +import { Origin } from "./env.js"; const contentApp = express(); const contentPort = 3000; -contentApp.all("*", handler); +contentApp.all("*", createHandler(Origin.main)); contentApp.listen(contentPort, () => { console.log(`Content app listening on port ${contentPort}`); }); -contentApp.listen(5042, () => { - console.log(`Sample app listening on port ${5042}`); + +const liveSampleApp = express(); +const liveSamplePort = 5042; + +liveSampleApp.all("*", createHandler(Origin.liveSamples)); +liveSampleApp.listen(liveSamplePort, () => { + console.log(`Sample app listening on port ${liveSamplePort}`); }); diff --git a/gcp/function/src/env.ts b/gcp/function/src/env.ts index 873f4619c0b5..7fb04aee5355 100644 --- a/gcp/function/src/env.ts +++ b/gcp/function/src/env.ts @@ -41,8 +41,7 @@ export const ORIGIN_INTERACTIVE_SAMPLES: string = "interactive-examples.mdn.mozilla.net"; export function origin(req: express.Request): Origin { - const host = req.headers.host || req.hostname; - switch (host) { + switch (req.hostname) { case ORIGIN_MAIN: return Origin.main; case ORIGIN_LIVE_SAMPLES: From 0f9cda33609cf3ceb0f62c7c878675eb82f02433 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 7 Mar 2023 12:14:08 +0100 Subject: [PATCH 022/343] fixup! chore(gcp/function): extract liveSampleApp --- gcp/function/src/app.ts | 4 +++- gcp/function/src/index.ts | 9 +++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 192fb7a0e123..917080cbc1bc 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -30,7 +30,9 @@ export function createHandler(o?: Origin) { return async ( req: express.Request, res: express.Response, - next: express.NextFunction + next: express.NextFunction = () => { + /* noop */ + } ) => { const rPath = req.path; const reqOrigin = o || origin(req); diff --git a/gcp/function/src/index.ts b/gcp/function/src/index.ts index af4394a37e76..3dc1e5d2aa21 100644 --- a/gcp/function/src/index.ts +++ b/gcp/function/src/index.ts @@ -1,8 +1,5 @@ -import { handler } from "./app.js"; +import { createHandler } from "./app.js"; import functions from "@google-cloud/functions-framework"; -functions.http("mdnHandler", (req, res) => - handler(req, res, () => { - /* noop */ - }) -); +const handler = createHandler(); +functions.http("mdnHandler", handler); From 9b3ef7214a043c6c5ffeb8ab880b0610770854e2 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 7 Mar 2023 12:35:38 +0100 Subject: [PATCH 023/343] refactor(gcp/function): extract resolveIndexHTML() --- gcp/function/src/handlers/content.ts | 18 +++++------------- gcp/function/src/utils.ts | 10 ++++++++++ 2 files changed, 15 insertions(+), 13 deletions(-) create mode 100644 gcp/function/src/utils.ts diff --git a/gcp/function/src/handlers/content.ts b/gcp/function/src/handlers/content.ts index af3e2e160567..2a92728edb7a 100644 --- a/gcp/function/src/handlers/content.ts +++ b/gcp/function/src/handlers/content.ts @@ -1,10 +1,10 @@ import type express from "express"; import * as path from "node:path"; -import { slugToFolder } from "@yari-internal/slug-utils"; import { withResponseHeaders } from "../headers.js"; import { responder } from "../source.js"; import { Source } from "../env.js"; import httpProxy from "http-proxy"; +import { resolveIndexHTML } from "../utils.js"; export function docs(): express.Handler { return responder({ @@ -18,12 +18,8 @@ export function docs(): express.Handler { autoRewrite: true, }); contentProxy.on("proxyReq", (proxyReq, req) => { - const rPath = req.url; - let folderName = slugToFolder(rPath || ""); - if (path.extname(folderName) === "") { - folderName = path.join(folderName, "index.html"); - } - proxyReq.path = path.join(proxyReq.path, folderName); + const resolvedPath = resolveIndexHTML(req.url || ""); + proxyReq.path = path.join(proxyReq.path, resolvedPath); }); return (req, res) => { contentProxy.web(req, res); @@ -31,12 +27,8 @@ export function docs(): express.Handler { }, file(source) { return (req, res) => { - const rPath = req.path; - const folderName = slugToFolder(rPath); - let filePath = path.join(source, folderName); - if (path.extname(filePath) === "") { - filePath = path.join(filePath, "index.html"); - } + const resolvedPath = resolveIndexHTML(req.path); + const filePath = path.join(source, resolvedPath); return withResponseHeaders(res, { csp: true, xFrame: true }).sendFile( filePath ); diff --git a/gcp/function/src/utils.ts b/gcp/function/src/utils.ts new file mode 100644 index 000000000000..5a18596d6a61 --- /dev/null +++ b/gcp/function/src/utils.ts @@ -0,0 +1,10 @@ +import { slugToFolder } from "@yari-internal/slug-utils"; +import * as path from "node:path"; + +export function resolveIndexHTML(pathOrUrl: string) { + let resolvedPath = slugToFolder(pathOrUrl); + if (path.extname(resolvedPath) === "") { + resolvedPath = path.join(resolvedPath, "index.html"); + } + return resolvedPath; +} From a40156f9375f115b5994a4cde7030c5ff34dd731 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 7 Mar 2023 12:35:58 +0100 Subject: [PATCH 024/343] fix(gcp/function): resolve index.html in client --- gcp/function/src/handlers/client.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gcp/function/src/handlers/client.ts b/gcp/function/src/handlers/client.ts index cb3becae98f5..e15ad9c1b019 100644 --- a/gcp/function/src/handlers/client.ts +++ b/gcp/function/src/handlers/client.ts @@ -3,6 +3,7 @@ import httpProxy from "http-proxy"; import * as path from "node:path"; import { Source } from "../env.js"; import { responder } from "../source.js"; +import { resolveIndexHTML } from "../utils.js"; export function client(): express.Handler { return responder({ @@ -15,13 +16,14 @@ export function client(): express.Handler { autoRewrite: true, }); return (req, res) => { + req.url = resolveIndexHTML(req.url); clientProxy.web(req, res); }; }, file(source) { return (req, res) => { - const rPath = req.path; - const filePath = path.join(source, rPath); + const resolvedPath = resolveIndexHTML(req.path); + const filePath = path.join(source, resolvedPath); res.sendFile(filePath); }; }, From 43f4ccc2c0551fbb43eb40b47aab819e38091852 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 7 Mar 2023 12:51:58 +0100 Subject: [PATCH 025/343] feat(gcp/function): add fundamental redirects --- gcp/function/src/app.ts | 2 ++ gcp/function/src/middlewares/fundamental.ts | 22 +++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 gcp/function/src/middlewares/fundamental.ts diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 917080cbc1bc..1f72e3f6ebf5 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -7,6 +7,7 @@ import { bcdApi } from "./handlers/bcdApi.js"; import { spa } from "./handlers/spa.js"; import { rumba } from "./handlers/rumba.js"; import { pathnameLC } from "./middlewares/pathnameLC.js"; +import { fundamental } from "./middlewares/fundamental.js"; const mainRouter = Router(); const docsHandler = docs(); @@ -16,6 +17,7 @@ mainRouter.all("/users/fxa/*", rumba); mainRouter.get("/[^/]+/plus/*", spa); mainRouter.get("/[^/]+/docs/*", docsHandler); mainRouter.get("/[^/]+/search-index.json", docsHandler); +mainRouter.use(fundamental); mainRouter.get("*", client()); const liveSampleRouter = Router(); diff --git a/gcp/function/src/middlewares/fundamental.ts b/gcp/function/src/middlewares/fundamental.ts new file mode 100644 index 000000000000..46a4e81a7788 --- /dev/null +++ b/gcp/function/src/middlewares/fundamental.ts @@ -0,0 +1,22 @@ +import type express from "express"; +import { resolveFundamental } from "@yari-internal/fundamental-redirects"; + +const THIRTY_DAYS = 3600 * 24 * 30; + +export function fundamental( + req: express.Request, + res: express.Response, + next: express.NextFunction +) { + const { url, status } = resolveFundamental(req.url); + console.log("fundamental", { url, status }); + if (url) { + res.set("Cache-Control", `max-age=${THIRTY_DAYS}`); + if (status) { + res.redirect(status, url); + } else { + res.redirect(url); + } + } + next(); +} From a44a83a4af2a055e452d336a4ffff71eeb6640f5 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 7 Mar 2023 14:36:14 +0100 Subject: [PATCH 026/343] feat(gcp): add env + build script --- .env.gcp | 36 ++++++++++++++++++++++++++++++++++++ gcp.sh | 21 +++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 .env.gcp create mode 100755 gcp.sh diff --git a/.env.gcp b/.env.gcp new file mode 100644 index 000000000000..6874b614d988 --- /dev/null +++ b/.env.gcp @@ -0,0 +1,36 @@ +CONTENT_ROOT=../content/files +CONTENT_TRANSLATED_ROOT=../translated-content/files +CONTRIBUTOR_SPOTLIGHT_ROOT=../mdn-contributor-spotlight/contributors + +BUILD_LIVE_SAMPLES_BASE_URL=https://yari-demos.developer.allizom.xyz + +BUILD_INTERACTIVE_EXAMPLES_BASE_URL=https://interactive-examples.developer.allizom.xyz + +BUILD_FLAW_LEVELS="*:ignore" + +REACT_APP_ENABLE_PLUS=false + +REACT_APP_DISABLE_AUTH=true + +REACT_APP_INTERACTIVE_EXAMPLES_BASE_URL=https://interactive-examples.developer.allizom.xyz + + +# Firefox Accounts and SubPlat settings +REACT_APP_FXA_SIGNIN_URL=/users/fxa/login/authenticate/ +REACT_APP_FXA_SETTINGS_URL=https://accounts.stage.mozaws.net/settings/ +REACT_APP_MDN_PLUS_SUBSCRIBE_URL=https://accounts.stage.mozaws.net/subscriptions/products/prod_Jtbg9tyGyLRuB0 +REACT_APP_MDN_PLUS_5M_PLAN=price_1JFoTYKb9q6OnNsLalexa03p +REACT_APP_MDN_PLUS_5Y_PLAN=price_1JpIPwKb9q6OnNsLJLsIqMp7 +REACT_APP_MDN_PLUS_10M_PLAN=price_1K6X7gKb9q6OnNsLi44HdLcC +REACT_APP_MDN_PLUS_10Y_PLAN=price_1K6X8VKb9q6OnNsLFlUcEiu4 + + +# Glean +REACT_APP_GLEAN_CHANNEL=stage +REACT_APP_GLEAN_ENABLED=false + +# Newsletter +REACT_APP_NEWSLETTER_ENABLED=false + +# Newsletter +REACT_APP_PLACEMENT_ENABLED=false diff --git a/gcp.sh b/gcp.sh new file mode 100755 index 000000000000..580435f1ad9d --- /dev/null +++ b/gcp.sh @@ -0,0 +1,21 @@ +export ENV_FILE='.env.gcp' + +bucket=${BUCKET:?} + +# Clean. +rm -rf client/build + +# Build. +echo "ENV_FILE=$ENV_FILE" +yarn build:sw +yarn build:prepare +yarn tool popularities +yarn build --locale en-us +yarn build --sitemap-index + +# Deploy content. +gsutil -m rsync -d -r client/build/ "gs://${bucket}/" + +# Deploy function. +set ENV_FILE= +cd gcp/function && npm i && npm run build-deploy-clean From d91e7946ac0d763df602fb3146bd5228491e8d7d Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 7 Mar 2023 14:53:52 +0100 Subject: [PATCH 027/343] refactor(gcp/function): extract prepare script --- gcp/function/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gcp/function/package.json b/gcp/function/package.json index 77131ecede68..e759865dae18 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -8,11 +8,13 @@ "type": "module", "main": "src/index.js", "scripts": { - "build": "tsc -b && rm -rf ./src/internal && mkdir ./src/internal && cp -R ../../libs/{constants,fundamental-redirects,locale-utils,slug-utils} ./src/internal/", + "build": "tsc -b", "build-deploy-clean": "npm-run-all -s build deploy clean", "clean": "tsc -b --clean", + "copy-internal": "rm -rf ./src/internal && mkdir ./src/internal && cp -R ../../libs/{constants,fundamental-redirects,locale-utils,slug-utils} ./src/internal/", "deploy": "gcloud functions deploy mdn-two --region europe-west3 --runtime nodejs18 --trigger-http --allow-unauthenticated", "package": "zip -r -X f.zip . -i package.json .env 'src/*.js' 'src/handlers/*.js' 'src/internal'", + "prepare": "[ ! -e ../../libs ] || npm run copy-internal", "start": "ts-node src/cli.ts" }, "dependencies": { From f578c1c14120f8c4bb49f816f44db918e9f1f834 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 7 Mar 2023 14:56:56 +0100 Subject: [PATCH 028/343] fix(scripts): add install:all:npm + add to prepare --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 10a0e1b939fc..394a2aede21c 100644 --- a/package.json +++ b/package.json @@ -27,10 +27,11 @@ "eslint": "eslint .", "filecheck": "ts-node filecheck/cli.ts", "install:all": "find . -mindepth 2 -name 'yarn.lock' ! -wholename '**/node_modules/**' -print0 | xargs -n1 -0 sh -cx 'yarn --cwd $(dirname $0) install'", + "install:all:npm": "find . -mindepth 2 -name 'package-lock.json' ! -wholename '**/node_modules/**' -print0 | xargs -n1 -0 sh -cx 'npm --prefix $(dirname $0) install'", "jest": "cross-env NODE_OPTIONS=--experimental-vm-modules jest", "m2h": "ts-node markdown/m2h/cli.ts", "prepack": "yarn build:dist", - "prepare": "husky install && yarn install:all", + "prepare": "husky install && yarn install:all && yarn install:all:npm", "prettier-check": "prettier --check .", "prettier-format": "prettier --write .", "start": "(test -f client/build/index.html || yarn build:client) && (test -f ssr/dist/main.js || yarn build:ssr) && (test -d client/build/en-us/_spas || yarn tool spas) && nf -j Procfile.start start", From 1ad80795c71b7d80e21c874ddfae0dfe4aa75157 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 7 Mar 2023 15:04:32 +0100 Subject: [PATCH 029/343] fixup! fix(scripts): add install:all:npm + add to prepare --- gcp/function/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/function/package.json b/gcp/function/package.json index e759865dae18..c6eb239d1469 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -11,7 +11,7 @@ "build": "tsc -b", "build-deploy-clean": "npm-run-all -s build deploy clean", "clean": "tsc -b --clean", - "copy-internal": "rm -rf ./src/internal && mkdir ./src/internal && cp -R ../../libs/{constants,fundamental-redirects,locale-utils,slug-utils} ./src/internal/", + "copy-internal": "rm -rf ./src/internal && mkdir ./src/internal && cp -R ../../libs/ ./src/internal/", "deploy": "gcloud functions deploy mdn-two --region europe-west3 --runtime nodejs18 --trigger-http --allow-unauthenticated", "package": "zip -r -X f.zip . -i package.json .env 'src/*.js' 'src/handlers/*.js' 'src/internal'", "prepare": "[ ! -e ../../libs ] || npm run copy-internal", From 9ceb539fab22d96526dd7fae02c60f9d08d5b898 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 7 Mar 2023 16:37:18 +0100 Subject: [PATCH 030/343] feat(gcp/function): build redirects map --- gcp/function/package.json | 4 +-- gcp/function/redirects.json | 1 + gcp/function/src/build.ts | 64 +++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 gcp/function/redirects.json create mode 100644 gcp/function/src/build.ts diff --git a/gcp/function/package.json b/gcp/function/package.json index c6eb239d1469..84a51a92a8bb 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -8,9 +8,9 @@ "type": "module", "main": "src/index.js", "scripts": { - "build": "tsc -b", + "build": "tsc -b && ts-node src/build.ts", "build-deploy-clean": "npm-run-all -s build deploy clean", - "clean": "tsc -b --clean", + "clean": "tsc -b --clean && git checkout -- redirects.json", "copy-internal": "rm -rf ./src/internal && mkdir ./src/internal && cp -R ../../libs/ ./src/internal/", "deploy": "gcloud functions deploy mdn-two --region europe-west3 --runtime nodejs18 --trigger-http --allow-unauthenticated", "package": "zip -r -X f.zip . -i package.json .env 'src/*.js' 'src/handlers/*.js' 'src/internal'", diff --git a/gcp/function/redirects.json b/gcp/function/redirects.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/gcp/function/redirects.json @@ -0,0 +1 @@ +{} diff --git a/gcp/function/src/build.ts b/gcp/function/src/build.ts new file mode 100644 index 000000000000..f0f757cde33a --- /dev/null +++ b/gcp/function/src/build.ts @@ -0,0 +1,64 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; + +import dotenv from "dotenv"; + +import { VALID_LOCALES } from "@yari-internal/constants"; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +const root = path.join(dirname, "..", "..", ".."); +dotenv.config({ + path: path.join(root, process.env["ENV_FILE"] || ".env"), +}); + +function buildRedirectsMap() { + const redirectMap = new Map(); + + ["CONTENT_ROOT", "CONTENT_TRANSLATED_ROOT"].forEach((envvar) => { + if (!process.env[envvar]) { + console.error(`Missing ENV variable: ${envvar}`); + return; + } + + const base = process.env[envvar]; + console.log(`${envvar} = ${base}`); + + for (const locale of VALID_LOCALES.keys()) { + const path = [ + // Absolute path. + `${base}/${locale}/_redirects.txt`, + `${base}/files/${locale}/_redirects.txt`, + // Relative path. + `${root}/${base}/${locale}/_redirects.txt`, + `${root}/${base}/files/${locale}/_redirects.txt`, + ].find((path) => fs.existsSync(path)); + + if (path) { + const content = fs.readFileSync(path, "utf-8"); + const lines = content.split("\n"); + const redirectLines = lines.filter( + (line) => line.startsWith("/") && line.includes("\t") + ); + for (const redirectLine of redirectLines) { + const [source, target] = redirectLine.split("\t", 2); + if (source && target) { + redirectMap.set(source.toLowerCase(), target); + } + } + console.log(`- ${path}: ${redirectLines.length} redirects`); + } + } + }); + + const output = "redirects.json"; + + fs.writeFileSync(output, JSON.stringify(Object.fromEntries(redirectMap))); + + const count = redirectMap.size; + const kb = Math.round(fs.statSync(output).size / 1024); + console.log(`Wrote ${count} redirects in ${kb} KB.`); +} + +buildRedirectsMap(); From ae122b5bc2234c159b367e1a24089ea3b89f3d15 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 7 Mar 2023 17:07:06 +0100 Subject: [PATCH 031/343] feat(gcp/function): merge redirects handling with fundamental --- gcp/function/src/app.ts | 4 +- gcp/function/src/middlewares/fundamental.ts | 22 -------- gcp/function/src/middlewares/redirects.ts | 57 +++++++++++++++++++++ 3 files changed, 59 insertions(+), 24 deletions(-) delete mode 100644 gcp/function/src/middlewares/fundamental.ts create mode 100644 gcp/function/src/middlewares/redirects.ts diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 1f72e3f6ebf5..0ba1e03c4600 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -7,17 +7,17 @@ import { bcdApi } from "./handlers/bcdApi.js"; import { spa } from "./handlers/spa.js"; import { rumba } from "./handlers/rumba.js"; import { pathnameLC } from "./middlewares/pathnameLC.js"; -import { fundamental } from "./middlewares/fundamental.js"; +import { redirects } from "./middlewares/redirects.js"; const mainRouter = Router(); const docsHandler = docs(); mainRouter.get("/bcd/api/*", bcdApi()); mainRouter.all("/api/*", rumba); mainRouter.all("/users/fxa/*", rumba); +mainRouter.use(redirects); mainRouter.get("/[^/]+/plus/*", spa); mainRouter.get("/[^/]+/docs/*", docsHandler); mainRouter.get("/[^/]+/search-index.json", docsHandler); -mainRouter.use(fundamental); mainRouter.get("*", client()); const liveSampleRouter = Router(); diff --git a/gcp/function/src/middlewares/fundamental.ts b/gcp/function/src/middlewares/fundamental.ts deleted file mode 100644 index 46a4e81a7788..000000000000 --- a/gcp/function/src/middlewares/fundamental.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type express from "express"; -import { resolveFundamental } from "@yari-internal/fundamental-redirects"; - -const THIRTY_DAYS = 3600 * 24 * 30; - -export function fundamental( - req: express.Request, - res: express.Response, - next: express.NextFunction -) { - const { url, status } = resolveFundamental(req.url); - console.log("fundamental", { url, status }); - if (url) { - res.set("Cache-Control", `max-age=${THIRTY_DAYS}`); - if (status) { - res.redirect(status, url); - } else { - res.redirect(url); - } - } - next(); -} diff --git a/gcp/function/src/middlewares/redirects.ts b/gcp/function/src/middlewares/redirects.ts new file mode 100644 index 000000000000..c34d1ec75449 --- /dev/null +++ b/gcp/function/src/middlewares/redirects.ts @@ -0,0 +1,57 @@ +import { createRequire } from "node:module"; + +import type express from "express"; + +import { resolveFundamental } from "@yari-internal/fundamental-redirects"; +import { decodePath } from "@yari-internal/slug-utils"; + +const require = createRequire(import.meta.url); +const REDIRECTS = require("../../redirects.json"); +const REDIRECT_SUFFIXES = ["/index.json", "/bcd.json", ""]; +const THIRTY_DAYS = 3600 * 24 * 30; + +export function redirects( + req: express.Request, + res: express.Response, + next: express.NextFunction +) { + const { url, status } = resolveFundamental(req.url); + if (url) { + res.set("Cache-Control", `max-age=${THIRTY_DAYS}`); + if (status) { + res.redirect(status, url); + } else { + res.redirect(url); + } + } + + // Important: The request.uri may be URI-encoded. + // Example: + // - Encoded: /zh-TW/docs/AJAX:%E4%B8%8A%E6%89%8B%E7%AF%87 + // - Decoded: /zh-TW/docs/AJAX:上手篇 + const decodedUri = decodePath(req.url); + const decodedUriLC = decodedUri.toLowerCase(); + + // Redirect moved pages (see `_redirects.txt` in content/translated-content). + // Example: + // - Source: /zh-TW/docs/AJAX:上手篇 + // - Target: /zh-TW/docs/Web/Guide/AJAX/Getting_Started + for (const suffix of REDIRECT_SUFFIXES) { + if (!decodedUriLC.endsWith(suffix)) { + continue; + } + const source = decodedUriLC.substring( + 0, + decodedUriLC.length - suffix.length + ); + if (typeof REDIRECTS[source] == "string") { + const target = REDIRECTS[source] + suffix; + console.log(req.url, target); + res.set("Cache-Control", `max-age=${THIRTY_DAYS}`); + res.redirect(301, target); + break; + } + } + + next(); +} From 2730945fa53b4b11d94be99b4bec1c5cfdbc3302 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 7 Mar 2023 17:15:07 +0100 Subject: [PATCH 032/343] chore(gcp/function): reduce memory to 128M --- gcp/function/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/function/package.json b/gcp/function/package.json index 84a51a92a8bb..3faa7390a3b8 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -12,7 +12,7 @@ "build-deploy-clean": "npm-run-all -s build deploy clean", "clean": "tsc -b --clean && git checkout -- redirects.json", "copy-internal": "rm -rf ./src/internal && mkdir ./src/internal && cp -R ../../libs/ ./src/internal/", - "deploy": "gcloud functions deploy mdn-two --region europe-west3 --runtime nodejs18 --trigger-http --allow-unauthenticated", + "deploy": "gcloud functions deploy mdn-two --region europe-west3 --memory=128MB --runtime nodejs18 --trigger-http --allow-unauthenticated", "package": "zip -r -X f.zip . -i package.json .env 'src/*.js' 'src/handlers/*.js' 'src/internal'", "prepare": "[ ! -e ../../libs ] || npm run copy-internal", "start": "ts-node src/cli.ts" From e68bf6ac96968f4f8bae76dec68f33d30a629ac1 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 7 Mar 2023 17:21:11 +0100 Subject: [PATCH 033/343] fix(gcp/function): return after redirect --- gcp/function/src/middlewares/redirects.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gcp/function/src/middlewares/redirects.ts b/gcp/function/src/middlewares/redirects.ts index c34d1ec75449..f996a115b03b 100644 --- a/gcp/function/src/middlewares/redirects.ts +++ b/gcp/function/src/middlewares/redirects.ts @@ -23,6 +23,8 @@ export function redirects( } else { res.redirect(url); } + next(); + return; } // Important: The request.uri may be URI-encoded. @@ -49,7 +51,8 @@ export function redirects( console.log(req.url, target); res.set("Cache-Control", `max-age=${THIRTY_DAYS}`); res.redirect(301, target); - break; + next(); + return; } } From a4b608265d02b943ada33024582445572eb6201b Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 7 Mar 2023 17:21:41 +0100 Subject: [PATCH 034/343] feat(gcp/function): strip multiple leading slashes --- gcp/function/src/middlewares/redirects.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/gcp/function/src/middlewares/redirects.ts b/gcp/function/src/middlewares/redirects.ts index f996a115b03b..f13a92f619d1 100644 --- a/gcp/function/src/middlewares/redirects.ts +++ b/gcp/function/src/middlewares/redirects.ts @@ -15,6 +15,24 @@ export function redirects( res: express.Response, next: express.NextFunction ) { + // If the URL was something like `https://domain/en-US/search/`, our code + // would make a that a redirect to `/en-US/search` (stripping the trailing slash). + // But if it was `https://domain//en-US/search/` it *would* make a redirect + // to `//en-US/search`. + // However, if pathname starts with `//` the Location header might look + // relative but it's actually an absolute URL. + // A 302 redirect from `https://domain//evil.com/` actually ends open + // opening `https://evil.com/` in the browser, because the browser will + // treat `//evil.com/ == https://evil.com/`. + // Prevent any pathnames that start with a double //. + // This essentially means that a request for `GET /////anything` becomes + // 302 with `Location: /anything`. + if (req.url.startsWith("//")) { + res.redirect(`/${req.url.replace(/^\/+/g, "")}`); + next(); + return; + } + const { url, status } = resolveFundamental(req.url); if (url) { res.set("Cache-Control", `max-age=${THIRTY_DAYS}`); From 454bb8dd0d4ffc10567ca0dcc26297d731cbd0c0 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 7 Mar 2023 17:32:53 +0100 Subject: [PATCH 035/343] fixup! feat(gcp/function): merge redirects handling with fundamental --- gcp/function/src/middlewares/redirects.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/gcp/function/src/middlewares/redirects.ts b/gcp/function/src/middlewares/redirects.ts index f13a92f619d1..43f210379fb5 100644 --- a/gcp/function/src/middlewares/redirects.ts +++ b/gcp/function/src/middlewares/redirects.ts @@ -66,7 +66,6 @@ export function redirects( ); if (typeof REDIRECTS[source] == "string") { const target = REDIRECTS[source] + suffix; - console.log(req.url, target); res.set("Cache-Control", `max-age=${THIRTY_DAYS}`); res.redirect(301, target); next(); From 63e5c448c4a09c178f8a91a5bd6c0ec35128a04b Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 7 Mar 2023 17:33:23 +0100 Subject: [PATCH 036/343] fix(gcp/function): use pathname for redirects --- gcp/function/src/middlewares/redirects.ts | 27 ++++++++++++++++------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/gcp/function/src/middlewares/redirects.ts b/gcp/function/src/middlewares/redirects.ts index 43f210379fb5..b9606498546f 100644 --- a/gcp/function/src/middlewares/redirects.ts +++ b/gcp/function/src/middlewares/redirects.ts @@ -15,6 +15,8 @@ export function redirects( res: express.Response, next: express.NextFunction ) { + const url = new URL(req.url, `${req.protocol}://${req.headers.host}`); + // If the URL was something like `https://domain/en-US/search/`, our code // would make a that a redirect to `/en-US/search` (stripping the trailing slash). // But if it was `https://domain//en-US/search/` it *would* make a redirect @@ -27,19 +29,28 @@ export function redirects( // Prevent any pathnames that start with a double //. // This essentially means that a request for `GET /////anything` becomes // 302 with `Location: /anything`. - if (req.url.startsWith("//")) { - res.redirect(`/${req.url.replace(/^\/+/g, "")}`); + if (url.pathname.startsWith("//")) { + const target = `/${url.pathname.replace(/^\/+/g, "")}`; + res.redirect(target); next(); return; } - const { url, status } = resolveFundamental(req.url); - if (url) { + const redirect = resolveFundamental(url.pathname); + if (redirect.url) { + // NOTE: The query string is not forwarded for document requests, + // as directed by their origin request policy, so it's safe to + // assume "request.querystring" is empty for document requests. + if (url.search) { + redirect.url += + (redirect.url.includes("?") ? "&" : "?") + url.search.substring(1); + } + res.set("Cache-Control", `max-age=${THIRTY_DAYS}`); - if (status) { - res.redirect(status, url); + if (redirect.status) { + res.redirect(redirect.status, redirect.url); } else { - res.redirect(url); + res.redirect(redirect.url); } next(); return; @@ -49,7 +60,7 @@ export function redirects( // Example: // - Encoded: /zh-TW/docs/AJAX:%E4%B8%8A%E6%89%8B%E7%AF%87 // - Decoded: /zh-TW/docs/AJAX:上手篇 - const decodedUri = decodePath(req.url); + const decodedUri = decodePath(url.pathname); const decodedUriLC = decodedUri.toLowerCase(); // Redirect moved pages (see `_redirects.txt` in content/translated-content). From 8c2e09726782ce42d98fdc8d79716543df017ee8 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 7 Mar 2023 23:59:43 +0100 Subject: [PATCH 037/343] feat(gcp/function): sync redirects middleware with lambda --- gcp/function/src/middlewares/redirects.ts | 156 +++++++++++++++++++--- 1 file changed, 134 insertions(+), 22 deletions(-) diff --git a/gcp/function/src/middlewares/redirects.ts b/gcp/function/src/middlewares/redirects.ts index b9606498546f..5b57e5157496 100644 --- a/gcp/function/src/middlewares/redirects.ts +++ b/gcp/function/src/middlewares/redirects.ts @@ -4,18 +4,72 @@ import type express from "express"; import { resolveFundamental } from "@yari-internal/fundamental-redirects"; import { decodePath } from "@yari-internal/slug-utils"; +import { DEFAULT_LOCALE, VALID_LOCALES } from "@yari-internal/constants"; const require = createRequire(import.meta.url); const REDIRECTS = require("../../redirects.json"); const REDIRECT_SUFFIXES = ["/index.json", "/bcd.json", ""]; const THIRTY_DAYS = 3600 * 24 * 30; +const NEEDS_LOCALE = /^\/(?:docs|search|settings|signin|signup|plus)(?:$|\/)/; +// Note that the keys of "VALID_LOCALES" are lowercase locales. +const LOCALE_URI_WITHOUT_TRAILING_SLASH = new Set( + [...VALID_LOCALES.keys()].map((locale) => `/${locale}`) +); +const LOCALE_URI_WITH_TRAILING_SLASH = new Set( + [...VALID_LOCALES.keys()].map((locale) => `/${locale}/`) +); +// TODO: The code that uses LEGACY_URI_NEEDING_TRAILING_SLASH should be +// temporary. For example, when we have moved to the Yari-built +// account settings page, we should add fundamental redirects +// for "/{locale}/account/?" and "/account/?" that redirect to +// "/{locale}/settings" and "/settings" respectively. The other +// cases can be either redirected or deleted eventually as well. +// The goal is to eventually remove the code that uses +// LEGACY_URI_NEEDING_TRAILING_SLASH. +const LEGACY_URI_NEEDING_TRAILING_SLASH = new RegExp( + `^(?:${[...LOCALE_URI_WITHOUT_TRAILING_SLASH].join( + "|" + )})?/(?:account|contribute|maintenance-mode|payments)/?$` +); export function redirects( req: express.Request, res: express.Response, next: express.NextFunction ) { + function redirect( + location: string, + { status = 302, cacheControlSeconds = 0 } = {} + ) { + let cacheControlValue; + if (cacheControlSeconds) { + cacheControlValue = `max-age=${cacheControlSeconds},public`; + } else { + cacheControlValue = "no-store"; + } + + res.set("Cache-Control", cacheControlValue); + + // We need to URL encode the pathname, but leave the query string as is. + // Suppose the old URL was `/search?q=text%2Dshadow` and all we need to do + // is to inject the locale to that URL, we should not URL encode the whole + // new URL otherwise you'd end up with `/en-US/search?q=text%252Dshadow` + // since the already encoded `%2D` would become `%252D` which is wrong and + // different. + const [pathname, querystring] = location.split("?", 2); + let newLocation = encodeURI(pathname || ""); + if (querystring) { + newLocation += `?${querystring}`; + } + + res.redirect(status, newLocation); + next(); + } + const url = new URL(req.url, `${req.protocol}://${req.headers.host}`); + let requestURI = url.pathname; + const requestURILowerCase = requestURI.toLowerCase(); + const qs = url.search; // If the URL was something like `https://domain/en-US/search/`, our code // would make a that a redirect to `/en-US/search` (stripping the trailing slash). @@ -29,34 +83,92 @@ export function redirects( // Prevent any pathnames that start with a double //. // This essentially means that a request for `GET /////anything` becomes // 302 with `Location: /anything`. - if (url.pathname.startsWith("//")) { - const target = `/${url.pathname.replace(/^\/+/g, "")}`; - res.redirect(target); - next(); - return; + if (requestURI.startsWith("//")) { + return redirect(`/${requestURI.replace(/^\/+/g, "")}`); } - const redirect = resolveFundamental(url.pathname); - if (redirect.url) { + const fundamentalRedirect = resolveFundamental(requestURI); + if (fundamentalRedirect.url) { // NOTE: The query string is not forwarded for document requests, // as directed by their origin request policy, so it's safe to // assume "request.querystring" is empty for document requests. if (url.search) { - redirect.url += - (redirect.url.includes("?") ? "&" : "?") + url.search.substring(1); + fundamentalRedirect.url += + (fundamentalRedirect.url.includes("?") ? "&" : "?") + + url.search.substring(1); } + return redirect(fundamentalRedirect.url, { + status: fundamentalRedirect.status, + cacheControlSeconds: THIRTY_DAYS, + }); + } - res.set("Cache-Control", `max-age=${THIRTY_DAYS}`); - if (redirect.status) { - res.redirect(redirect.status, redirect.url); - } else { - res.redirect(redirect.url); - } - next(); - return; + // Do we need to insert the locale? If we do, trim a trailing slash + // to avoid a double redirect, except when requesting the home page. + if ( + requestURI === "" || + requestURI === "/" || + NEEDS_LOCALE.test(requestURILowerCase) + ) { + const path = requestURI.endsWith("/") + ? requestURI.slice(0, -1) + : requestURI; + // Note that "getLocale" only returns valid locales, never a retired locale. + const locale = DEFAULT_LOCALE; // TODO getLocale(req.cookies); + // The only time we actually want a trailing slash is when the URL is just + // the locale. E.g. `/en-US/` (not `/en-US`) + return redirect(`/${locale}${path || "/"}` + qs); + } + + // At this point, the URI is guaranteed to start with a forward slash. + const uriParts = requestURI.split("/"); + const uriFirstPart = uriParts[1] ?? ""; + const uriFirstPartLC = uriFirstPart.toLowerCase(); + + // Do we need to redirect to the properly-cased locale? We also ensure + // here that requests for the home page have a trailing slash, while + // all others do not. + if ( + VALID_LOCALES.has(uriFirstPartLC) && + uriFirstPart !== VALID_LOCALES.get(uriFirstPartLC) + ) { + // Assemble the rest of the path without a trailing slash. + const extra = uriParts.slice(2).filter(Boolean).join("/"); + return redirect(`/${VALID_LOCALES.get(uriFirstPartLC)}/${extra}${qs}`); + } + + // Handle cases related to the presence or absence of a trailing-slash. + if (LOCALE_URI_WITHOUT_TRAILING_SLASH.has(requestURILowerCase)) { + // Home page requests are the special case on MDN. They should + // always have a trailing slash. So a home page URL without a + // trailing slash should redirect to the same URL with a + // trailing slash. When the redirected home-page request is + // processed by this Lambda function, note that we'll remove + // the trailing slash before the request reaches S3 (see below). + return redirect(requestURI + "/" + qs, { + cacheControlSeconds: THIRTY_DAYS, + }); + } else if (LOCALE_URI_WITH_TRAILING_SLASH.has(requestURILowerCase)) { + // We've received a proper request for a locale's home page (i.e., + // it has a traling slash), but since that request will be served + // from S3, we need to strip the trailing slash before it reaches + // S3. This is required because we store the home pages in S3 as + // their path name itself, for example "en-us" for the English home + // page, not "en-us/index.html", which is what S3 would look for if + // we left the trailing slash. + requestURI = requestURI.slice(0, -1); + } else if ( + requestURI.endsWith("/") && + !LEGACY_URI_NEEDING_TRAILING_SLASH.test(requestURILowerCase) + ) { + // All other requests with a trailing slash should redirect to the + // same URL without the trailing slash. + return redirect(requestURI.slice(0, -1) + qs, { + cacheControlSeconds: THIRTY_DAYS, + }); } - // Important: The request.uri may be URI-encoded. + // Important: The requestURI may be URI-encoded. // Example: // - Encoded: /zh-TW/docs/AJAX:%E4%B8%8A%E6%89%8B%E7%AF%87 // - Decoded: /zh-TW/docs/AJAX:上手篇 @@ -77,10 +189,10 @@ export function redirects( ); if (typeof REDIRECTS[source] == "string") { const target = REDIRECTS[source] + suffix; - res.set("Cache-Control", `max-age=${THIRTY_DAYS}`); - res.redirect(301, target); - next(); - return; + return redirect(target, { + status: 301, + cacheControlSeconds: THIRTY_DAYS, + }); } } From 0a9918de50c7e1ff4b9306da9c03a54768efef84 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 8 Mar 2023 15:24:17 +0100 Subject: [PATCH 038/343] feat(locale-utils): support express.Request --- libs/locale-utils/index.js | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/libs/locale-utils/index.js b/libs/locale-utils/index.js index 52f6b8168059..ec2e53780259 100644 --- a/libs/locale-utils/index.js +++ b/libs/locale-utils/index.js @@ -12,13 +12,25 @@ 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) { + let value = headers["cookie"]; + // Cookies are present in the HTTP header "Cookie" that may be present multiple times. // This utility function parses occurrences of that header and splits out all the cookies and their values // A simple object is returned that allows easy access by cookie name: e.g. cookies["nonce"] - if (!headers["cookie"]) { + if (!value) { return {}; } - const cookies = headers["cookie"].reduce( + + if (typeof value === "string") { + // Express. + value = [ + { + value, + }, + ]; + } + + const cookies = value.reduce( (reduced, header) => Object.assign(reduced, parse(header.value)), {} ); @@ -26,13 +38,25 @@ function extractCookiesFromHeaders(headers) { return cookies; } -function getCookie(headers, cookieKey) { - return extractCookiesFromHeaders(headers)[cookieKey]; +function getCookie(request, cookieKey) { + return extractCookiesFromHeaders(request.headers)[cookieKey]; +} + +function getAcceptLanguage(request) { + const acceptLangHeaders = request.headers["accept-language"]; + + if (typeof acceptLangHeaders === "string") { + // Express. + return acceptLangHeaders; + } + + const { value = null } = (acceptLangHeaders && acceptLangHeaders[0]) || {}; + return value; } export function getLocale(request, fallback = DEFAULT_LOCALE) { // First try by cookie. - const cookieLocale = getCookie(request.headers, PREFERRED_LOCALE_COOKIE_NAME); + const cookieLocale = getCookie(request, PREFERRED_LOCALE_COOKIE_NAME); if (cookieLocale) { // If it's valid, stick to it. if (VALID_LOCALES.has(cookieLocale.toLowerCase())) { @@ -41,8 +65,7 @@ export function getLocale(request, fallback = DEFAULT_LOCALE) { } // Each header in request.headers is always a list of objects. - const acceptLangHeaders = request.headers["accept-language"]; - const { value = null } = (acceptLangHeaders && acceptLangHeaders[0]) || {}; + const value = getAcceptLanguage(request); const locale = value && acceptLanguageParser.pick(VALID_LOCALES_LIST, value, { loose: true }); From 8a9ddd5242ad447d5fc1ab528a3ca821652df6bd Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 8 Mar 2023 15:25:57 +0100 Subject: [PATCH 039/343] fix(gcp/function): reuse getLocale() --- gcp/function/src/middlewares/redirects.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gcp/function/src/middlewares/redirects.ts b/gcp/function/src/middlewares/redirects.ts index 5b57e5157496..c29695c3361a 100644 --- a/gcp/function/src/middlewares/redirects.ts +++ b/gcp/function/src/middlewares/redirects.ts @@ -3,8 +3,9 @@ import { createRequire } from "node:module"; import type express from "express"; import { resolveFundamental } from "@yari-internal/fundamental-redirects"; +import { getLocale } from "@yari-internal/locale-utils"; import { decodePath } from "@yari-internal/slug-utils"; -import { DEFAULT_LOCALE, VALID_LOCALES } from "@yari-internal/constants"; +import { VALID_LOCALES } from "@yari-internal/constants"; const require = createRequire(import.meta.url); const REDIRECTS = require("../../redirects.json"); @@ -114,7 +115,7 @@ export function redirects( ? requestURI.slice(0, -1) : requestURI; // Note that "getLocale" only returns valid locales, never a retired locale. - const locale = DEFAULT_LOCALE; // TODO getLocale(req.cookies); + const locale = getLocale(req); // The only time we actually want a trailing slash is when the URL is just // the locale. E.g. `/en-US/` (not `/en-US`) return redirect(`/${locale}${path || "/"}` + qs); From 5d10d9441a257d3920f116f7715f225e3f31baef Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 8 Mar 2023 15:26:12 +0100 Subject: [PATCH 040/343] Revert "chore(gcp/function): reduce memory to 128M" This reverts commit 2730945fa53b4b11d94be99b4bec1c5cfdbc3302. --- gcp/function/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/function/package.json b/gcp/function/package.json index 3faa7390a3b8..84a51a92a8bb 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -12,7 +12,7 @@ "build-deploy-clean": "npm-run-all -s build deploy clean", "clean": "tsc -b --clean && git checkout -- redirects.json", "copy-internal": "rm -rf ./src/internal && mkdir ./src/internal && cp -R ../../libs/ ./src/internal/", - "deploy": "gcloud functions deploy mdn-two --region europe-west3 --memory=128MB --runtime nodejs18 --trigger-http --allow-unauthenticated", + "deploy": "gcloud functions deploy mdn-two --region europe-west3 --runtime nodejs18 --trigger-http --allow-unauthenticated", "package": "zip -r -X f.zip . -i package.json .env 'src/*.js' 'src/handlers/*.js' 'src/internal'", "prepare": "[ ! -e ../../libs ] || npm run copy-internal", "start": "ts-node src/cli.ts" From cae4d8feeb90c79a4a60f3b37bc98942b81b52c2 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 8 Mar 2023 15:47:08 +0100 Subject: [PATCH 041/343] fix(locale-utils): add dependencies --- libs/locale-utils/package.json | 6 +++++- libs/locale-utils/yarn.lock | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 libs/locale-utils/yarn.lock diff --git a/libs/locale-utils/package.json b/libs/locale-utils/package.json index 2fa2fb0d4057..8540243c4e0c 100644 --- a/libs/locale-utils/package.json +++ b/libs/locale-utils/package.json @@ -6,5 +6,9 @@ "type": "module", "exports": "./index.js", "main": "index.js", - "types": "index.d.ts" + "types": "index.d.ts", + "dependencies": { + "accept-language-parser": "^1.5.0", + "cookie": "^0.5.0" + } } diff --git a/libs/locale-utils/yarn.lock b/libs/locale-utils/yarn.lock new file mode 100644 index 000000000000..0c19b72a365e --- /dev/null +++ b/libs/locale-utils/yarn.lock @@ -0,0 +1,13 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +accept-language-parser@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/accept-language-parser/-/accept-language-parser-1.5.0.tgz#8877c54040a8dcb59e0a07d9c1fde42298334791" + integrity sha512-QhyTbMLYo0BBGg1aWbeMG4ekWtds/31BrEU+DONOg/7ax23vxpL03Pb7/zBmha2v7vdD3AyzZVWBVGEZxKOXWw== + +cookie@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== From 8e613df8c02da8a5cbf4813e9a2c408e493566c0 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 8 Mar 2023 16:06:20 +0100 Subject: [PATCH 042/343] fixup! fix(locale-utils): add dependencies --- libs/locale-utils/package-lock.json | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 libs/locale-utils/package-lock.json diff --git a/libs/locale-utils/package-lock.json b/libs/locale-utils/package-lock.json new file mode 100644 index 000000000000..97db7867fc2a --- /dev/null +++ b/libs/locale-utils/package-lock.json @@ -0,0 +1,32 @@ +{ + "name": "@yari-internal/locale-utils", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@yari-internal/locale-utils", + "version": "0.0.1", + "license": "MPL-2.0", + "dependencies": { + "accept-language-parser": "^1.5.0", + "cookie": "^0.5.0" + } + }, + "node_modules/accept-language-parser": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/accept-language-parser/-/accept-language-parser-1.5.0.tgz", + "integrity": "sha512-QhyTbMLYo0BBGg1aWbeMG4ekWtds/31BrEU+DONOg/7ax23vxpL03Pb7/zBmha2v7vdD3AyzZVWBVGEZxKOXWw==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + } + } +} From 9cb5bfd9be20fa9bc3678bbb332ba1dd38a0e850 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 8 Mar 2023 16:08:13 +0100 Subject: [PATCH 043/343] fix(gcp/function): update package-lock --- gcp/function/package-lock.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index 063527001819..aa07091d6418 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -282,6 +282,11 @@ "resolved": "src/internal/slug-utils", "link": true }, + "node_modules/accept-language-parser": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/accept-language-parser/-/accept-language-parser-1.5.0.tgz", + "integrity": "sha512-QhyTbMLYo0BBGg1aWbeMG4ekWtds/31BrEU+DONOg/7ax23vxpL03Pb7/zBmha2v7vdD3AyzZVWBVGEZxKOXWw==" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -2422,7 +2427,11 @@ "src/internal/locale-utils": { "name": "@yari-internal/locale-utils", "version": "0.0.1", - "license": "MPL-2.0" + "license": "MPL-2.0", + "dependencies": { + "accept-language-parser": "^1.5.0", + "cookie": "^0.5.0" + } }, "src/internal/slug-utils": { "name": "@yari-internal/slug-utils", From 9af71c79d2450330d5e2157a6bf4848cbfe79b4c Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 8 Mar 2023 17:40:29 +0100 Subject: [PATCH 044/343] fix(gcp/function): do not resolveIndexHTML in /static --- gcp/function/src/handlers/client.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gcp/function/src/handlers/client.ts b/gcp/function/src/handlers/client.ts index e15ad9c1b019..48bdd83c2292 100644 --- a/gcp/function/src/handlers/client.ts +++ b/gcp/function/src/handlers/client.ts @@ -16,7 +16,9 @@ export function client(): express.Handler { autoRewrite: true, }); return (req, res) => { - req.url = resolveIndexHTML(req.url); + if (!req.url.startsWith("/static/")) { + req.url = resolveIndexHTML(req.url); + } clientProxy.web(req, res); }; }, From d798a2c01421b3c93cabd310715dce2a17a21c30 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 8 Mar 2023 22:50:42 +0100 Subject: [PATCH 045/343] ci: add xyz-build --- .github/workflows/xyz-build.yml | 157 ++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 .github/workflows/xyz-build.yml diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml new file mode 100644 index 000000000000..f41a48747916 --- /dev/null +++ b/.github/workflows/xyz-build.yml @@ -0,0 +1,157 @@ +name: XYZ Build (GCP) + +on: + push: + branches: + - gcp + + workflow_call: + secrets: + GCP_PROJECT_NAME: + required: true + GCS_BUCKET: + required: true + WIP_PROJECT_ID: + required: true +jobs: + build: + environment: xyz + permissions: + contents: read + id-token: write + + runs-on: ubuntu-latest + + # Only run the scheduled workflows on the main repo. + if: github.repository == 'mdn/yari' + + steps: + - uses: actions/checkout@v3 + + - uses: actions/checkout@v3 + with: + repository: mdn/content + path: mdn/content + # Yes, this means fetch EVERY COMMIT EVER. + # It's probably not sustainable in the far future (e.g. past 2021) + # but for now it's good enough. We'll need all the history + # so we can figure out each document's last-modified date. + fetch-depth: 0 + + - uses: actions/checkout@v3 + with: + repository: mdn/translated-content + path: mdn/translated-content + # See matching warning for mdn/content checkout step + fetch-depth: 0 + + - uses: actions/checkout@v3 + with: + repository: mdn/mdn-contributor-spotlight + path: mdn/mdn-contributor-spotlight + + - name: Setup Node.js environment + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: yarn + + - name: Install all yarn packages + run: yarn --frozen-lockfile + + - name: Print information about CPU + run: cat /proc/cpuinfo + + - name: Build everything + env: + # Remember, the mdn/content repo got cloned into `pwd` into a + # sub-folder called "mdn/content" + CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files + CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files + CONTRIBUTOR_SPOTLIGHT_ROOT: ${{ github.workspace }}/mdn/mdn-contributor-spotlight/contributors + + # The default for this environment variable is geared for writers + # (aka. local development). Usually defaults are supposed to be for + # secure production but this is an exception and default + # is not insecure. + BUILD_LIVE_SAMPLES_BASE_URL: https://yari-demos.developer.allizom.xyz + + # Use the stage version of interactive examples. + BUILD_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.developer.allizom.xyz + + # Now is not the time to worry about flaws. + BUILD_FLAW_LEVELS: "*:ignore" + + # This is the Google Analytics account ID for developer.mozilla.org + # If it's used on other domains (e.g. stage or dev builds), it's OK + # because ultimately Google Analytics will filter it out since the + # origin domain isn't what that account expects. + #BUILD_GOOGLE_ANALYTICS_ACCOUNT: UA-36116321-5 + + # This enables the Plus call-to-action banner and the Plus landing page + REACT_APP_ENABLE_PLUS: false + + # This adds the ability to sign in (stage only for now) + REACT_APP_DISABLE_AUTH: true + + # Use the stage version of interactive examples in react app + REACT_APP_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.developer.allizom.xyz + + # Firefox Accounts and SubPlat settings + REACT_APP_FXA_SIGNIN_URL: /users/fxa/login/authenticate/ + REACT_APP_FXA_SETTINGS_URL: https://accounts.stage.mozaws.net/settings/ + REACT_APP_MDN_PLUS_SUBSCRIBE_URL: https://accounts.stage.mozaws.net/subscriptions/products/prod_Jtbg9tyGyLRuB0 + REACT_APP_MDN_PLUS_5M_PLAN: price_1JFoTYKb9q6OnNsLalexa03p + REACT_APP_MDN_PLUS_5Y_PLAN: price_1JpIPwKb9q6OnNsLJLsIqMp7 + REACT_APP_MDN_PLUS_10M_PLAN: price_1K6X7gKb9q6OnNsLi44HdLcC + REACT_APP_MDN_PLUS_10Y_PLAN: price_1K6X8VKb9q6OnNsLFlUcEiu4 + + # Surveys. + REACT_APP_SURVEY_START_CONTENT_DISCOVERY_2023: 0 # stage + REACT_APP_SURVEY_END_CONTENT_DISCOVERY_2023: 1677672000000 # (new Date("2023-03-01 12:00:00Z")).getTime() + REACT_APP_SURVEY_RATE_FROM_CONTENT_DISCOVERY_2023: 0.0 + REACT_APP_SURVEY_RATE_TILL_CONTENT_DISCOVERY_2023: 0.05 # 5% + + # Telemetry. + REACT_APP_GLEAN_CHANNEL: stage + REACT_APP_GLEAN_ENABLED: false + + # Newsletter + REACT_APP_NEWSLETTER_ENABLED: false + + # Placement + REACT_APP_PLACEMENT_ENABLED: false + + run: | + + # Info about which CONTENT_* environment variables were set and to what. + echo "CONTENT_ROOT=$CONTENT_ROOT" + echo "CONTENT_TRANSLATED_ROOT=$CONTENT_TRANSLATED_ROOT" + # Build the ServiceWorker first + yarn build:sw + yarn build:prepare + + # (July 15, 2021) This is a temporary solution. This should become an + # integrated part of 'build:prepare'. + # See https://github.com/mdn/yari/issues/4217 + yarn tool popularities + + yarn build --locale en-us + + du -sh client/build + + # Generate sitemap index file + yarn build --sitemap-index + + - name: Authenticate with GCP + uses: google-github-actions/auth@v0 + with: + token_format: access_token + service_account: deploy-xyz-yari@${{ secrets.GCP_PROJECT_NAME }}.iam.gserviceaccount.com + workload_identity_provider: projects/${{ secrets.WIP_PROJECT_ID }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions + + - name: Setup gcloud + uses: google-github-actions/setup-gcloud@v1 + + - name: Sync build with GCS bucket + run: gsutil -m rsync -d -r client/build/ gs://${{ secrets.GCS_BUCKET }}/ From 1d473310da1f8c129f9ca36ac6f8a4ac5ca521dc Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 8 Mar 2023 23:46:08 +0100 Subject: [PATCH 046/343] ci(xyz-build): deploy function --- .github/workflows/xyz-build.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index f41a48747916..18af569f81ea 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -155,3 +155,19 @@ jobs: - name: Sync build with GCS bucket run: gsutil -m rsync -d -r client/build/ gs://${{ secrets.GCS_BUCKET }}/ + + + - name: Deploy Function + env: + CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files + CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files + ORIGIN_MAIN: developer.allizom.xyz + ORIGIN_LIVE_SAMPLES: yari-demos.developer.allizom.xyz + SOURCE_CONTENT: https://storage.googleapis.com/fiji-mdn-content/ + SOURCE_CLIENT: https://storage.googleapis.com/fiji-mdn-content/ + SOURCE_INTERACTIVE_SAMPLES: https://storage.googleapis.com/fiji-mdn-interactive-examples/ + run: | + cd gcp/function + npm ci + npm run build + npm run deploy From c50d9c189a6cdef920140e807ae47f15e81539b1 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 9 Mar 2023 00:31:46 +0100 Subject: [PATCH 047/343] ci(xyz-build): enable plus/auth --- .github/workflows/xyz-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 18af569f81ea..65270ecfcae1 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -89,10 +89,10 @@ jobs: #BUILD_GOOGLE_ANALYTICS_ACCOUNT: UA-36116321-5 # This enables the Plus call-to-action banner and the Plus landing page - REACT_APP_ENABLE_PLUS: false + REACT_APP_ENABLE_PLUS: true # This adds the ability to sign in (stage only for now) - REACT_APP_DISABLE_AUTH: true + REACT_APP_DISABLE_AUTH: false # Use the stage version of interactive examples in react app REACT_APP_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.developer.allizom.xyz From 4c08a84d671a58b2a6e5ee07a357718e9d843b4d Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 9 Mar 2023 13:35:51 +0100 Subject: [PATCH 048/343] chore(gcp/function): remove spa handler --- gcp/function/src/app.ts | 2 -- gcp/function/src/handlers/spa.ts | 16 ---------------- 2 files changed, 18 deletions(-) delete mode 100644 gcp/function/src/handlers/spa.ts diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 0ba1e03c4600..cba536638283 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -4,7 +4,6 @@ import { Origin, origin } from "./env.js"; import { docs } from "./handlers/content.js"; import { client } from "./handlers/client.js"; import { bcdApi } from "./handlers/bcdApi.js"; -import { spa } from "./handlers/spa.js"; import { rumba } from "./handlers/rumba.js"; import { pathnameLC } from "./middlewares/pathnameLC.js"; import { redirects } from "./middlewares/redirects.js"; @@ -15,7 +14,6 @@ mainRouter.get("/bcd/api/*", bcdApi()); mainRouter.all("/api/*", rumba); mainRouter.all("/users/fxa/*", rumba); mainRouter.use(redirects); -mainRouter.get("/[^/]+/plus/*", spa); mainRouter.get("/[^/]+/docs/*", docsHandler); mainRouter.get("/[^/]+/search-index.json", docsHandler); mainRouter.get("*", client()); diff --git a/gcp/function/src/handlers/spa.ts b/gcp/function/src/handlers/spa.ts deleted file mode 100644 index 2cfb1db68cf1..000000000000 --- a/gcp/function/src/handlers/spa.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type express from "express"; -import * as path from "node:path"; -import { slugToFolder } from "@yari-internal/slug-utils"; -import { withResponseHeaders } from "../headers.js"; - -export async function spa(req: express.Request, res: express.Response) { - const rPath = req.path; - const folderName = slugToFolder(rPath); - let filePath = path.join("/tmp/bar/", folderName); - if (path.extname(filePath) === "") { - filePath = path.join(filePath, "index.html"); - } - return withResponseHeaders(res, { csp: true, xFrame: true }).sendFile( - filePath - ); -} From af1caffc3354bec41ab37a19117ea8be390a1c18 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 9 Mar 2023 17:13:43 +0100 Subject: [PATCH 049/343] ci(xyz-build): run gsutil with -q This should suppress printing every file path. (At the cost of not seeing the progress.) --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 65270ecfcae1..6af25cbf6316 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -154,7 +154,7 @@ jobs: uses: google-github-actions/setup-gcloud@v1 - name: Sync build with GCS bucket - run: gsutil -m rsync -d -r client/build/ gs://${{ secrets.GCS_BUCKET }}/ + run: gsutil -q -m rsync -d -r client/build/ gs://${{ secrets.GCS_BUCKET }}/ - name: Deploy Function From 3838de237500370784a7f9f266caaf68e232ee8e Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 9 Mar 2023 18:19:34 +0100 Subject: [PATCH 050/343] chore(gcp): remove content build from gcp.sh --- .env.gcp | 36 ------------------------------------ gcp.sh | 20 -------------------- 2 files changed, 56 deletions(-) delete mode 100644 .env.gcp diff --git a/.env.gcp b/.env.gcp deleted file mode 100644 index 6874b614d988..000000000000 --- a/.env.gcp +++ /dev/null @@ -1,36 +0,0 @@ -CONTENT_ROOT=../content/files -CONTENT_TRANSLATED_ROOT=../translated-content/files -CONTRIBUTOR_SPOTLIGHT_ROOT=../mdn-contributor-spotlight/contributors - -BUILD_LIVE_SAMPLES_BASE_URL=https://yari-demos.developer.allizom.xyz - -BUILD_INTERACTIVE_EXAMPLES_BASE_URL=https://interactive-examples.developer.allizom.xyz - -BUILD_FLAW_LEVELS="*:ignore" - -REACT_APP_ENABLE_PLUS=false - -REACT_APP_DISABLE_AUTH=true - -REACT_APP_INTERACTIVE_EXAMPLES_BASE_URL=https://interactive-examples.developer.allizom.xyz - - -# Firefox Accounts and SubPlat settings -REACT_APP_FXA_SIGNIN_URL=/users/fxa/login/authenticate/ -REACT_APP_FXA_SETTINGS_URL=https://accounts.stage.mozaws.net/settings/ -REACT_APP_MDN_PLUS_SUBSCRIBE_URL=https://accounts.stage.mozaws.net/subscriptions/products/prod_Jtbg9tyGyLRuB0 -REACT_APP_MDN_PLUS_5M_PLAN=price_1JFoTYKb9q6OnNsLalexa03p -REACT_APP_MDN_PLUS_5Y_PLAN=price_1JpIPwKb9q6OnNsLJLsIqMp7 -REACT_APP_MDN_PLUS_10M_PLAN=price_1K6X7gKb9q6OnNsLi44HdLcC -REACT_APP_MDN_PLUS_10Y_PLAN=price_1K6X8VKb9q6OnNsLFlUcEiu4 - - -# Glean -REACT_APP_GLEAN_CHANNEL=stage -REACT_APP_GLEAN_ENABLED=false - -# Newsletter -REACT_APP_NEWSLETTER_ENABLED=false - -# Newsletter -REACT_APP_PLACEMENT_ENABLED=false diff --git a/gcp.sh b/gcp.sh index 580435f1ad9d..f53eaf7f7eb4 100755 --- a/gcp.sh +++ b/gcp.sh @@ -1,21 +1 @@ -export ENV_FILE='.env.gcp' - -bucket=${BUCKET:?} - -# Clean. -rm -rf client/build - -# Build. -echo "ENV_FILE=$ENV_FILE" -yarn build:sw -yarn build:prepare -yarn tool popularities -yarn build --locale en-us -yarn build --sitemap-index - -# Deploy content. -gsutil -m rsync -d -r client/build/ "gs://${bucket}/" - -# Deploy function. -set ENV_FILE= cd gcp/function && npm i && npm run build-deploy-clean From 2a6d9a6c1eec8bf6ac8fa990a2cddd79b8e558d3 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 9 Mar 2023 18:54:47 +0100 Subject: [PATCH 051/343] chore(gcp/function): add @types/http-proxy --- gcp/function/package-lock.json | 10 ++++++++++ gcp/function/package.json | 1 + 2 files changed, 11 insertions(+) diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index aa07091d6418..b80fa89e74ef 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -20,6 +20,7 @@ "sanitize-filename": "^1.6.3" }, "devDependencies": { + "@types/http-proxy": "^1.17.10", "npm-run-all": "^4.1.5", "ts-node": "^10.9.1", "typescript": "^4.9.5" @@ -232,6 +233,15 @@ "@types/range-parser": "*" } }, + "node_modules/@types/http-proxy": { + "version": "1.17.10", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.10.tgz", + "integrity": "sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", diff --git a/gcp/function/package.json b/gcp/function/package.json index 84a51a92a8bb..0788d6d4c1a3 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -29,6 +29,7 @@ "sanitize-filename": "^1.6.3" }, "devDependencies": { + "@types/http-proxy": "^1.17.10", "npm-run-all": "^4.1.5", "ts-node": "^10.9.1", "typescript": "^4.9.5" From c17bfe70eb53fd120aeffc798af4cd5e9871cc7c Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 9 Mar 2023 18:56:09 +0100 Subject: [PATCH 052/343] chore(gcp/function): add @swc/core --- gcp/function/package-lock.json | 187 +++++++++++++++++++++++++++++++++ gcp/function/package.json | 1 + 2 files changed, 188 insertions(+) diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index b80fa89e74ef..adbb26491e4e 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -20,6 +20,7 @@ "sanitize-filename": "^1.6.3" }, "devDependencies": { + "@swc/core": "^1.3.38", "@types/http-proxy": "^1.17.10", "npm-run-all": "^4.1.5", "ts-node": "^10.9.1", @@ -171,6 +172,192 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@swc/core": { + "version": "1.3.38", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.38.tgz", + "integrity": "sha512-AiEVehRFws//AiiLx9DPDp1WDXt+yAoGD1kMYewhoF6QLdTz8AtYu6i8j/yAxk26L8xnegy0CDwcNnub9qenyQ==", + "dev": true, + "hasInstallScript": true, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.3.38", + "@swc/core-darwin-x64": "1.3.38", + "@swc/core-linux-arm-gnueabihf": "1.3.38", + "@swc/core-linux-arm64-gnu": "1.3.38", + "@swc/core-linux-arm64-musl": "1.3.38", + "@swc/core-linux-x64-gnu": "1.3.38", + "@swc/core-linux-x64-musl": "1.3.38", + "@swc/core-win32-arm64-msvc": "1.3.38", + "@swc/core-win32-ia32-msvc": "1.3.38", + "@swc/core-win32-x64-msvc": "1.3.38" + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.3.38", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.38.tgz", + "integrity": "sha512-4ZTJJ/cR0EsXW5UxFCifZoGfzQ07a8s4ayt1nLvLQ5QoB1GTAf9zsACpvWG8e7cmCR0L76R5xt8uJuyr+noIXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.3.38", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.38.tgz", + "integrity": "sha512-Kim727rNo4Dl8kk0CR8aJQe4zFFtsT1TZGlNrNMUgN1WC3CRX7dLZ6ZJi/VVcTG1cbHp5Fp3mUzwHsMxEh87Mg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.3.38", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.38.tgz", + "integrity": "sha512-yaRdnPNU2enlJDRcIMvYVSyodY+Amhf5QuXdUbAj6rkDD6wUs/s9C6yPYrFDmoTltrG+nBv72mUZj+R46wVfSw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.3.38", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.38.tgz", + "integrity": "sha512-iNY1HqKo/wBSu3QOGBUlZaLdBP/EHcwNjBAqIzpb8J64q2jEN02RizqVW0mDxyXktJ3lxr3g7VW9uqklMeXbjQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.3.38", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.38.tgz", + "integrity": "sha512-LJCFgLZoPRkPCPmux+Q5ctgXRp6AsWhvWuY61bh5bIPBDlaG9pZk94DeHyvtiwT0syhTtXb2LieBOx6NqN3zeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.3.38", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.38.tgz", + "integrity": "sha512-hRQGRIWHmv2PvKQM/mMV45mVXckM2+xLB8TYLLgUG66mmtyGTUJPyxjnJkbI86WNGqo18k+lAuMG2mn6QmzYwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.3.38", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.38.tgz", + "integrity": "sha512-PTYSqtsIfPHLKDDNbueI5e0sc130vyHRiFOeeC6qqzA2FAiVvIxuvXHLr0soPvKAR1WyhtYmFB9QarcctemL2w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.3.38", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.38.tgz", + "integrity": "sha512-9lHfs5TPNs+QdkyZFhZledSmzBEbqml/J1rqPSb9Fy8zB6QlspixE6OLZ3nTlUOdoGWkcTTdrOn77Sd7YGf1AA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.3.38", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.38.tgz", + "integrity": "sha512-SbL6pfA2lqvDKnwTHwOfKWvfHAdcbAwJS4dBkFidr7BiPTgI5Uk8wAPcRb8mBECpmIa9yFo+N0cAFRvMnf+cNw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.3.38", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.38.tgz", + "integrity": "sha512-UFveLrL6eGvViOD8OVqUQa6QoQwdqwRvLtL5elF304OT8eCPZa8BhuXnWk25X8UcOyns8gFcb8Fhp3oaLi/Rlw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", diff --git a/gcp/function/package.json b/gcp/function/package.json index 0788d6d4c1a3..429d4d44b50d 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -29,6 +29,7 @@ "sanitize-filename": "^1.6.3" }, "devDependencies": { + "@swc/core": "^1.3.38", "@types/http-proxy": "^1.17.10", "npm-run-all": "^4.1.5", "ts-node": "^10.9.1", From 40bbcf37b0b1469f52bf6c25ec8205ecd22dc338 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 9 Mar 2023 20:45:26 +0100 Subject: [PATCH 053/343] fix(gcp/function): omit trailing slash in cp command --- gcp/function/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/function/package.json b/gcp/function/package.json index 429d4d44b50d..022c49142c3c 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -11,7 +11,7 @@ "build": "tsc -b && ts-node src/build.ts", "build-deploy-clean": "npm-run-all -s build deploy clean", "clean": "tsc -b --clean && git checkout -- redirects.json", - "copy-internal": "rm -rf ./src/internal && mkdir ./src/internal && cp -R ../../libs/ ./src/internal/", + "copy-internal": "rm -rf ./src/internal && cp -R ../../libs ./src/internal", "deploy": "gcloud functions deploy mdn-two --region europe-west3 --runtime nodejs18 --trigger-http --allow-unauthenticated", "package": "zip -r -X f.zip . -i package.json .env 'src/*.js' 'src/handlers/*.js' 'src/internal'", "prepare": "[ ! -e ../../libs ] || npm run copy-internal", From b2d8b54cc2d7c034428ab5aaec26dcc4d149b267 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 9 Mar 2023 20:52:41 +0100 Subject: [PATCH 054/343] fixup! ci(xyz-build): deploy function --- .github/workflows/xyz-build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 6af25cbf6316..ee0ac65d18df 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -156,7 +156,6 @@ jobs: - name: Sync build with GCS bucket run: gsutil -q -m rsync -d -r client/build/ gs://${{ secrets.GCS_BUCKET }}/ - - name: Deploy Function env: CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files From 78e2f3091694e9d8c6519f5d41768e372b2e26cb Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 10 Mar 2023 09:19:42 +0100 Subject: [PATCH 055/343] ci(xyz-build): temporarily deploy function only --- .github/workflows/xyz-build.yml | 107 +------------------------------- 1 file changed, 1 insertion(+), 106 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index ee0ac65d18df..aefc3d5dbba2 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -32,117 +32,15 @@ jobs: with: repository: mdn/content path: mdn/content - # Yes, this means fetch EVERY COMMIT EVER. - # It's probably not sustainable in the far future (e.g. past 2021) - # but for now it's good enough. We'll need all the history - # so we can figure out each document's last-modified date. - fetch-depth: 0 - uses: actions/checkout@v3 with: repository: mdn/translated-content path: mdn/translated-content - # See matching warning for mdn/content checkout step - fetch-depth: 0 - - - uses: actions/checkout@v3 - with: - repository: mdn/mdn-contributor-spotlight - path: mdn/mdn-contributor-spotlight - - - name: Setup Node.js environment - uses: actions/setup-node@v3 - with: - node-version: 18 - cache: yarn - - - name: Install all yarn packages - run: yarn --frozen-lockfile - name: Print information about CPU run: cat /proc/cpuinfo - - name: Build everything - env: - # Remember, the mdn/content repo got cloned into `pwd` into a - # sub-folder called "mdn/content" - CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files - CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files - CONTRIBUTOR_SPOTLIGHT_ROOT: ${{ github.workspace }}/mdn/mdn-contributor-spotlight/contributors - - # The default for this environment variable is geared for writers - # (aka. local development). Usually defaults are supposed to be for - # secure production but this is an exception and default - # is not insecure. - BUILD_LIVE_SAMPLES_BASE_URL: https://yari-demos.developer.allizom.xyz - - # Use the stage version of interactive examples. - BUILD_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.developer.allizom.xyz - - # Now is not the time to worry about flaws. - BUILD_FLAW_LEVELS: "*:ignore" - - # This is the Google Analytics account ID for developer.mozilla.org - # If it's used on other domains (e.g. stage or dev builds), it's OK - # because ultimately Google Analytics will filter it out since the - # origin domain isn't what that account expects. - #BUILD_GOOGLE_ANALYTICS_ACCOUNT: UA-36116321-5 - - # This enables the Plus call-to-action banner and the Plus landing page - REACT_APP_ENABLE_PLUS: true - - # This adds the ability to sign in (stage only for now) - REACT_APP_DISABLE_AUTH: false - - # Use the stage version of interactive examples in react app - REACT_APP_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.developer.allizom.xyz - - # Firefox Accounts and SubPlat settings - REACT_APP_FXA_SIGNIN_URL: /users/fxa/login/authenticate/ - REACT_APP_FXA_SETTINGS_URL: https://accounts.stage.mozaws.net/settings/ - REACT_APP_MDN_PLUS_SUBSCRIBE_URL: https://accounts.stage.mozaws.net/subscriptions/products/prod_Jtbg9tyGyLRuB0 - REACT_APP_MDN_PLUS_5M_PLAN: price_1JFoTYKb9q6OnNsLalexa03p - REACT_APP_MDN_PLUS_5Y_PLAN: price_1JpIPwKb9q6OnNsLJLsIqMp7 - REACT_APP_MDN_PLUS_10M_PLAN: price_1K6X7gKb9q6OnNsLi44HdLcC - REACT_APP_MDN_PLUS_10Y_PLAN: price_1K6X8VKb9q6OnNsLFlUcEiu4 - - # Surveys. - REACT_APP_SURVEY_START_CONTENT_DISCOVERY_2023: 0 # stage - REACT_APP_SURVEY_END_CONTENT_DISCOVERY_2023: 1677672000000 # (new Date("2023-03-01 12:00:00Z")).getTime() - REACT_APP_SURVEY_RATE_FROM_CONTENT_DISCOVERY_2023: 0.0 - REACT_APP_SURVEY_RATE_TILL_CONTENT_DISCOVERY_2023: 0.05 # 5% - - # Telemetry. - REACT_APP_GLEAN_CHANNEL: stage - REACT_APP_GLEAN_ENABLED: false - - # Newsletter - REACT_APP_NEWSLETTER_ENABLED: false - - # Placement - REACT_APP_PLACEMENT_ENABLED: false - - run: | - - # Info about which CONTENT_* environment variables were set and to what. - echo "CONTENT_ROOT=$CONTENT_ROOT" - echo "CONTENT_TRANSLATED_ROOT=$CONTENT_TRANSLATED_ROOT" - # Build the ServiceWorker first - yarn build:sw - yarn build:prepare - - # (July 15, 2021) This is a temporary solution. This should become an - # integrated part of 'build:prepare'. - # See https://github.com/mdn/yari/issues/4217 - yarn tool popularities - - yarn build --locale en-us - - du -sh client/build - - # Generate sitemap index file - yarn build --sitemap-index - - name: Authenticate with GCP uses: google-github-actions/auth@v0 with: @@ -153,9 +51,6 @@ jobs: - name: Setup gcloud uses: google-github-actions/setup-gcloud@v1 - - name: Sync build with GCS bucket - run: gsutil -q -m rsync -d -r client/build/ gs://${{ secrets.GCS_BUCKET }}/ - - name: Deploy Function env: CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files @@ -165,8 +60,8 @@ jobs: SOURCE_CONTENT: https://storage.googleapis.com/fiji-mdn-content/ SOURCE_CLIENT: https://storage.googleapis.com/fiji-mdn-content/ SOURCE_INTERACTIVE_SAMPLES: https://storage.googleapis.com/fiji-mdn-interactive-examples/ + working-directory: gcp/function run: | - cd gcp/function npm ci npm run build npm run deploy From 6158b3e4bf4248f2a009038f5bfebf85ac9c1caa Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 10 Mar 2023 09:47:42 +0100 Subject: [PATCH 056/343] ci(xyz-build): specify project_id --- .github/workflows/xyz-build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index aefc3d5dbba2..07bf635ca175 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -50,6 +50,8 @@ jobs: - name: Setup gcloud uses: google-github-actions/setup-gcloud@v1 + with: + project_id: '${{ secrets.GCP_PROJECT_NAME }}' - name: Deploy Function env: From a7f05b5648337df36d4f874840ff1b77a3aa07ce Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 10 Mar 2023 10:06:19 +0100 Subject: [PATCH 057/343] Revert "ci(xyz-build): specify project_id" This reverts commit 6158b3e4bf4248f2a009038f5bfebf85ac9c1caa. --- .github/workflows/xyz-build.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 07bf635ca175..aefc3d5dbba2 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -50,8 +50,6 @@ jobs: - name: Setup gcloud uses: google-github-actions/setup-gcloud@v1 - with: - project_id: '${{ secrets.GCP_PROJECT_NAME }}' - name: Deploy Function env: From c0638e2089b0925fe5b83aaf0544afbdd8174544 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 10 Mar 2023 10:10:40 +0100 Subject: [PATCH 058/343] Revert "ci(xyz-build): temporarily deploy function only" This reverts commit 78e2f3091694e9d8c6519f5d41768e372b2e26cb. --- .github/workflows/xyz-build.yml | 107 +++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index aefc3d5dbba2..ee0ac65d18df 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -32,15 +32,117 @@ jobs: with: repository: mdn/content path: mdn/content + # Yes, this means fetch EVERY COMMIT EVER. + # It's probably not sustainable in the far future (e.g. past 2021) + # but for now it's good enough. We'll need all the history + # so we can figure out each document's last-modified date. + fetch-depth: 0 - uses: actions/checkout@v3 with: repository: mdn/translated-content path: mdn/translated-content + # See matching warning for mdn/content checkout step + fetch-depth: 0 + + - uses: actions/checkout@v3 + with: + repository: mdn/mdn-contributor-spotlight + path: mdn/mdn-contributor-spotlight + + - name: Setup Node.js environment + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: yarn + + - name: Install all yarn packages + run: yarn --frozen-lockfile - name: Print information about CPU run: cat /proc/cpuinfo + - name: Build everything + env: + # Remember, the mdn/content repo got cloned into `pwd` into a + # sub-folder called "mdn/content" + CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files + CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files + CONTRIBUTOR_SPOTLIGHT_ROOT: ${{ github.workspace }}/mdn/mdn-contributor-spotlight/contributors + + # The default for this environment variable is geared for writers + # (aka. local development). Usually defaults are supposed to be for + # secure production but this is an exception and default + # is not insecure. + BUILD_LIVE_SAMPLES_BASE_URL: https://yari-demos.developer.allizom.xyz + + # Use the stage version of interactive examples. + BUILD_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.developer.allizom.xyz + + # Now is not the time to worry about flaws. + BUILD_FLAW_LEVELS: "*:ignore" + + # This is the Google Analytics account ID for developer.mozilla.org + # If it's used on other domains (e.g. stage or dev builds), it's OK + # because ultimately Google Analytics will filter it out since the + # origin domain isn't what that account expects. + #BUILD_GOOGLE_ANALYTICS_ACCOUNT: UA-36116321-5 + + # This enables the Plus call-to-action banner and the Plus landing page + REACT_APP_ENABLE_PLUS: true + + # This adds the ability to sign in (stage only for now) + REACT_APP_DISABLE_AUTH: false + + # Use the stage version of interactive examples in react app + REACT_APP_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.developer.allizom.xyz + + # Firefox Accounts and SubPlat settings + REACT_APP_FXA_SIGNIN_URL: /users/fxa/login/authenticate/ + REACT_APP_FXA_SETTINGS_URL: https://accounts.stage.mozaws.net/settings/ + REACT_APP_MDN_PLUS_SUBSCRIBE_URL: https://accounts.stage.mozaws.net/subscriptions/products/prod_Jtbg9tyGyLRuB0 + REACT_APP_MDN_PLUS_5M_PLAN: price_1JFoTYKb9q6OnNsLalexa03p + REACT_APP_MDN_PLUS_5Y_PLAN: price_1JpIPwKb9q6OnNsLJLsIqMp7 + REACT_APP_MDN_PLUS_10M_PLAN: price_1K6X7gKb9q6OnNsLi44HdLcC + REACT_APP_MDN_PLUS_10Y_PLAN: price_1K6X8VKb9q6OnNsLFlUcEiu4 + + # Surveys. + REACT_APP_SURVEY_START_CONTENT_DISCOVERY_2023: 0 # stage + REACT_APP_SURVEY_END_CONTENT_DISCOVERY_2023: 1677672000000 # (new Date("2023-03-01 12:00:00Z")).getTime() + REACT_APP_SURVEY_RATE_FROM_CONTENT_DISCOVERY_2023: 0.0 + REACT_APP_SURVEY_RATE_TILL_CONTENT_DISCOVERY_2023: 0.05 # 5% + + # Telemetry. + REACT_APP_GLEAN_CHANNEL: stage + REACT_APP_GLEAN_ENABLED: false + + # Newsletter + REACT_APP_NEWSLETTER_ENABLED: false + + # Placement + REACT_APP_PLACEMENT_ENABLED: false + + run: | + + # Info about which CONTENT_* environment variables were set and to what. + echo "CONTENT_ROOT=$CONTENT_ROOT" + echo "CONTENT_TRANSLATED_ROOT=$CONTENT_TRANSLATED_ROOT" + # Build the ServiceWorker first + yarn build:sw + yarn build:prepare + + # (July 15, 2021) This is a temporary solution. This should become an + # integrated part of 'build:prepare'. + # See https://github.com/mdn/yari/issues/4217 + yarn tool popularities + + yarn build --locale en-us + + du -sh client/build + + # Generate sitemap index file + yarn build --sitemap-index + - name: Authenticate with GCP uses: google-github-actions/auth@v0 with: @@ -51,6 +153,9 @@ jobs: - name: Setup gcloud uses: google-github-actions/setup-gcloud@v1 + - name: Sync build with GCS bucket + run: gsutil -q -m rsync -d -r client/build/ gs://${{ secrets.GCS_BUCKET }}/ + - name: Deploy Function env: CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files @@ -60,8 +165,8 @@ jobs: SOURCE_CONTENT: https://storage.googleapis.com/fiji-mdn-content/ SOURCE_CLIENT: https://storage.googleapis.com/fiji-mdn-content/ SOURCE_INTERACTIVE_SAMPLES: https://storage.googleapis.com/fiji-mdn-interactive-examples/ - working-directory: gcp/function run: | + cd gcp/function npm ci npm run build npm run deploy From 4826f9ed926575268f34e2350892f7f0f0345f80 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 10 Mar 2023 10:12:34 +0100 Subject: [PATCH 059/343] ci(xyz-build): use working-directory instead of cd --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index ee0ac65d18df..ed2a866bc27e 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -165,8 +165,8 @@ jobs: SOURCE_CONTENT: https://storage.googleapis.com/fiji-mdn-content/ SOURCE_CLIENT: https://storage.googleapis.com/fiji-mdn-content/ SOURCE_INTERACTIVE_SAMPLES: https://storage.googleapis.com/fiji-mdn-interactive-examples/ + working-directory: gcp/function run: | - cd gcp/function npm ci npm run build npm run deploy From 2919fb758928e928fd093d25be837007c5a1ee35 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 10 Mar 2023 10:54:30 +0100 Subject: [PATCH 060/343] chore(gcp/function): do not upload .env files --- gcp/function/.gcloudignore | 1 + 1 file changed, 1 insertion(+) diff --git a/gcp/function/.gcloudignore b/gcp/function/.gcloudignore index e481753cb246..e419315ad70a 100644 --- a/gcp/function/.gcloudignore +++ b/gcp/function/.gcloudignore @@ -6,6 +6,7 @@ # For more information, run: # $ gcloud topic gcloudignore # +.env* .gcloudignore # If you would like to upload your .git directory, .gitignore file or files # from your .gitignore file, remove the corresponding line From 9f1e51a9e2e1fd0cf836ede3d515b29e41e50b6c Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 10 Mar 2023 10:57:36 +0100 Subject: [PATCH 061/343] ci(xyz-build): avoid concurrent execution --- .github/workflows/xyz-build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index ed2a866bc27e..db589b3c1b54 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -16,6 +16,8 @@ on: jobs: build: environment: xyz + concurrency: + group: ${{ github.workflow }} permissions: contents: read id-token: write From 8aea37f84e0d803e689a3c479cc3e9561df8f7ad Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 10 Mar 2023 10:58:33 +0100 Subject: [PATCH 062/343] ci(xyz-build): remove non-build env vars --- .github/workflows/xyz-build.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index db589b3c1b54..f1dfa0f62768 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -162,11 +162,6 @@ jobs: env: CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files - ORIGIN_MAIN: developer.allizom.xyz - ORIGIN_LIVE_SAMPLES: yari-demos.developer.allizom.xyz - SOURCE_CONTENT: https://storage.googleapis.com/fiji-mdn-content/ - SOURCE_CLIENT: https://storage.googleapis.com/fiji-mdn-content/ - SOURCE_INTERACTIVE_SAMPLES: https://storage.googleapis.com/fiji-mdn-interactive-examples/ working-directory: gcp/function run: | npm ci From 07174c8aa6bd2d765a1d2f5c3c3b413dd6ad7f2f Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 10 Mar 2023 18:41:07 +0100 Subject: [PATCH 063/343] chore(gcp/function): rename {redirect => contentOriginRequest} --- gcp/function/src/app.ts | 4 ++-- .../middlewares/{redirects.ts => content-origin-request.ts} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename gcp/function/src/middlewares/{redirects.ts => content-origin-request.ts} (99%) diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index cba536638283..c6dbcaa6191d 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -6,14 +6,14 @@ import { client } from "./handlers/client.js"; import { bcdApi } from "./handlers/bcdApi.js"; import { rumba } from "./handlers/rumba.js"; import { pathnameLC } from "./middlewares/pathnameLC.js"; -import { redirects } from "./middlewares/redirects.js"; +import { contentOriginRequest } from "./middlewares/content-origin-request.js"; const mainRouter = Router(); const docsHandler = docs(); mainRouter.get("/bcd/api/*", bcdApi()); mainRouter.all("/api/*", rumba); mainRouter.all("/users/fxa/*", rumba); -mainRouter.use(redirects); +mainRouter.use(contentOriginRequest); mainRouter.get("/[^/]+/docs/*", docsHandler); mainRouter.get("/[^/]+/search-index.json", docsHandler); mainRouter.get("*", client()); diff --git a/gcp/function/src/middlewares/redirects.ts b/gcp/function/src/middlewares/content-origin-request.ts similarity index 99% rename from gcp/function/src/middlewares/redirects.ts rename to gcp/function/src/middlewares/content-origin-request.ts index c29695c3361a..710e89a0ea39 100644 --- a/gcp/function/src/middlewares/redirects.ts +++ b/gcp/function/src/middlewares/content-origin-request.ts @@ -33,7 +33,7 @@ const LEGACY_URI_NEEDING_TRAILING_SLASH = new RegExp( )})?/(?:account|contribute|maintenance-mode|payments)/?$` ); -export function redirects( +export function contentOriginRequest( req: express.Request, res: express.Response, next: express.NextFunction From 2a0a63c52d05616776ccd7ba118849c44196604b Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 10 Mar 2023 21:55:22 +0100 Subject: [PATCH 064/343] feat(gcp/function): resolve relative source paths --- gcp/function/src/env.ts | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/gcp/function/src/env.ts b/gcp/function/src/env.ts index 7fb04aee5355..8170017a7ac3 100644 --- a/gcp/function/src/env.ts +++ b/gcp/function/src/env.ts @@ -53,21 +53,33 @@ export function origin(req: express.Request): Origin { } } -export const SOURCE_CONTENT: string = +function resolveSource(pathOrUrl: string) { + if (pathOrUrl.startsWith(".")) { + return path.resolve(path.join(cwd(), pathOrUrl)); + } else { + return pathOrUrl; + } +} + +export const SOURCE_CONTENT: string = resolveSource( process.env["SOURCE_CONTENT"] || - process.env["BUILD_OUT_ROOT"] || - "https://developer.mozilla.org"; -export const SOURCE_LIVE_SAMPLES: string = + process.env["BUILD_OUT_ROOT"] || + "https://developer.mozilla.org" +); +export const SOURCE_LIVE_SAMPLES: string = resolveSource( process.env["SOURCE_LIVE_SAMPLES"] || - process.env["BUILD_OUT_ROOT"] || - "https://yari-demos.prod.mdn.mozit.cloud"; + process.env["BUILD_OUT_ROOT"] || + "https://yari-demos.prod.mdn.mozit.cloud" +); export const SOURCE_BCD_API: string = process.env["SOURCE_BCD_API"] || "https://developer.mozilla.org"; -export const SOURCE_CLIENT: string = - process.env["SOURCE_CLIENT"] || "https://developer.mozilla.org"; -export const SOURCE_INTERACTIVE_SAMPLES: string = +export const SOURCE_CLIENT: string = resolveSource( + process.env["SOURCE_CLIENT"] || "https://developer.mozilla.org" +); +export const SOURCE_INTERACTIVE_SAMPLES: string = resolveSource( process.env["SOURCE_INTERACTIVE_SAMPLES"] || - "https://interactive-examples.mdn.mozilla.net"; + "https://interactive-examples.mdn.mozilla.net" +); export const SOURCE_RUMBA: string = process.env["SOURCE_RUMBA"] || "https://developer.mozilla.org"; From f6548931e5514c53b84c44aee21ac298f75fb219 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 10 Mar 2023 22:08:33 +0100 Subject: [PATCH 065/343] feat(gcp/function): always add response headers Replaces the "Content-Origin-Response" lambda function. --- gcp/function/src/handlers/bcdApi.ts | 4 ++- gcp/function/src/handlers/client.ts | 7 ++++- gcp/function/src/handlers/content.ts | 10 ++++--- gcp/function/src/headers.ts | 41 ++++++++++++++++++++++++++-- 4 files changed, 53 insertions(+), 9 deletions(-) diff --git a/gcp/function/src/handlers/bcdApi.ts b/gcp/function/src/handlers/bcdApi.ts index 59bc4b4ba918..b16c680f1d2d 100644 --- a/gcp/function/src/handlers/bcdApi.ts +++ b/gcp/function/src/handlers/bcdApi.ts @@ -3,6 +3,7 @@ import httpProxy from "http-proxy"; import * as path from "node:path"; import { Source } from "../env.js"; import { responder } from "../source.js"; +import { withResponseHeaders, withProxyResponseHeaders } from "../headers.js"; export function bcdApi(): express.Handler { return responder({ @@ -14,13 +15,14 @@ export function bcdApi(): express.Handler { target: source, autoRewrite: true, }); + bcdProxy.on("proxyRes", withProxyResponseHeaders); return (req, res) => bcdProxy.web(req, res); }, file(source) { return (req, res) => { const rPath = req.path; const filePath = path.join(source, rPath); - res.sendFile(filePath); + return withResponseHeaders(res).sendFile(filePath); }; }, }); diff --git a/gcp/function/src/handlers/client.ts b/gcp/function/src/handlers/client.ts index 48bdd83c2292..9200af06cfe3 100644 --- a/gcp/function/src/handlers/client.ts +++ b/gcp/function/src/handlers/client.ts @@ -4,6 +4,7 @@ import * as path from "node:path"; import { Source } from "../env.js"; import { responder } from "../source.js"; import { resolveIndexHTML } from "../utils.js"; +import { withResponseHeaders, withProxyResponseHeaders } from "../headers.js"; export function client(): express.Handler { return responder({ @@ -15,6 +16,7 @@ export function client(): express.Handler { target: source, autoRewrite: true, }); + clientProxy.on("proxyRes", withProxyResponseHeaders); return (req, res) => { if (!req.url.startsWith("/static/")) { req.url = resolveIndexHTML(req.url); @@ -26,7 +28,10 @@ export function client(): express.Handler { return (req, res) => { const resolvedPath = resolveIndexHTML(req.path); const filePath = path.join(source, resolvedPath); - res.sendFile(filePath); + return withResponseHeaders(res, { + csp: resolvedPath.endsWith(".html"), + xFrame: true, + }).sendFile(filePath); }; }, }); diff --git a/gcp/function/src/handlers/content.ts b/gcp/function/src/handlers/content.ts index 2a92728edb7a..11c592d41a97 100644 --- a/gcp/function/src/handlers/content.ts +++ b/gcp/function/src/handlers/content.ts @@ -1,6 +1,6 @@ import type express from "express"; import * as path from "node:path"; -import { withResponseHeaders } from "../headers.js"; +import { withResponseHeaders, withProxyResponseHeaders } from "../headers.js"; import { responder } from "../source.js"; import { Source } from "../env.js"; import httpProxy from "http-proxy"; @@ -21,6 +21,7 @@ export function docs(): express.Handler { const resolvedPath = resolveIndexHTML(req.url || ""); proxyReq.path = path.join(proxyReq.path, resolvedPath); }); + contentProxy.on("proxyRes", withProxyResponseHeaders); return (req, res) => { contentProxy.web(req, res); }; @@ -29,9 +30,10 @@ export function docs(): express.Handler { return (req, res) => { const resolvedPath = resolveIndexHTML(req.path); const filePath = path.join(source, resolvedPath); - return withResponseHeaders(res, { csp: true, xFrame: true }).sendFile( - filePath - ); + return withResponseHeaders(res, { + csp: resolvedPath.endsWith(".html"), + xFrame: true, + }).sendFile(filePath); }; }, }); diff --git a/gcp/function/src/headers.ts b/gcp/function/src/headers.ts index be3713c04cf4..6069c4600517 100644 --- a/gcp/function/src/headers.ts +++ b/gcp/function/src/headers.ts @@ -1,11 +1,47 @@ +import type { IncomingMessage, ServerResponse } from "node:http"; import type express from "express"; + import { CSP_VALUE } from "@yari-internal/constants"; + import { RUNTIME_ENV } from "./env.js"; +export function withProxyResponseHeaders( + _proxyRes: IncomingMessage, + req: IncomingMessage, + res: ServerResponse +): ServerResponse { + const isLiveSampleURI = req.url?.includes("/_sample_.") ?? false; + + setResponseHeaders((name, value) => res.setHeader(name, value), { + csp: + !isLiveSampleURI && + parseContentType(_proxyRes.headers["content-type"]).startsWith( + "text/html" + ), + xFrame: !isLiveSampleURI, + }); + + return res; +} + +function parseContentType(value: unknown): string { + const firstValue = Array.isArray(value) ? value[0] ?? "" : value; + + return typeof firstValue === "string" ? firstValue : ""; +} + export function withResponseHeaders( res: express.Response, - { csp = false, xFrame = false }: { csp?: boolean; xFrame?: boolean } = {} + options?: { csp?: boolean; xFrame?: boolean } ): express.Response { + setResponseHeaders((name, value) => res.set(name, value), options ?? {}); + return res; +} + +export function setResponseHeaders( + setHeader: (name: string, value: string) => void, + { csp = true, xFrame = true }: { csp?: boolean; xFrame?: boolean } +): void { [ ["X-XSS-Protection", "1; mode=block"], ["X-Content-Type-Options", "nosniff"], @@ -14,8 +50,7 @@ export function withResponseHeaders( ? [["Content-Security-Policy", CSP_VALUE]] : []), ...(xFrame ? [["X-Frame-Options", "DENY"]] : []), - ].forEach(([k, v]) => k && v && res.append(k, v)); - return res; + ].forEach(([k, v]) => k && v && setHeader(k, v)); } export function country(res: express.Request): string { From 7e738bf2f05068353b68b89708d8c8c95c9ffe7e Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 10 Mar 2023 23:18:14 +0100 Subject: [PATCH 066/343] ci(xyz-build): use stage interactive-examples --- .github/workflows/xyz-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index f1dfa0f62768..792129225605 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -79,7 +79,7 @@ jobs: BUILD_LIVE_SAMPLES_BASE_URL: https://yari-demos.developer.allizom.xyz # Use the stage version of interactive examples. - BUILD_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.developer.allizom.xyz + BUILD_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net # Now is not the time to worry about flaws. BUILD_FLAW_LEVELS: "*:ignore" @@ -97,7 +97,7 @@ jobs: REACT_APP_DISABLE_AUTH: false # Use the stage version of interactive examples in react app - REACT_APP_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.developer.allizom.xyz + REACT_APP_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net # Firefox Accounts and SubPlat settings REACT_APP_FXA_SIGNIN_URL: /users/fxa/login/authenticate/ From 9b669eacf20d16d3568d5f404ac25f5ea66751bc Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 10 Mar 2023 23:21:19 +0100 Subject: [PATCH 067/343] chore(csp): group by env, add xyz urls --- libs/constants/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/constants/index.js b/libs/constants/index.js index 74c7384086d4..1da1e7c89438 100644 --- a/libs/constants/index.js +++ b/libs/constants/index.js @@ -110,6 +110,7 @@ export const CSP_DIRECTIVES = { "yari-demos.prod.mdn.mozit.cloud", "mdn.mozillademos.org", "yari-demos.stage.mdn.mozit.cloud", + "yari-demos.developer.allizom.xyz", "jsfiddle.net", "www.youtube-nocookie.com", From 4fd1a5633ca2c93aa1db9d57a264910fd1e5a889 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 10 Mar 2023 23:21:50 +0100 Subject: [PATCH 068/343] chore(csp): add xyz urls --- libs/constants/index.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/libs/constants/index.js b/libs/constants/index.js index 1da1e7c89438..34ce8886fe1a 100644 --- a/libs/constants/index.js +++ b/libs/constants/index.js @@ -103,15 +103,22 @@ export const CSP_DIRECTIVES = { "frame-src": [ "'self'", + // Prod. "interactive-examples.mdn.mozilla.net", "interactive-examples.prod.mdn.mozilla.net", - "interactive-examples.stage.mdn.mozilla.net", - "mdn.github.io", "yari-demos.prod.mdn.mozit.cloud", - "mdn.mozillademos.org", + + // Stage. + "interactive-examples.stage.mdn.mozilla.net", "yari-demos.stage.mdn.mozit.cloud", + + // XYZ. + "interactive-examples.developer.allizom.xyz", "yari-demos.developer.allizom.xyz", + "mdn.github.io", + "mdn.mozillademos.org", + "jsfiddle.net", "www.youtube-nocookie.com", "codepen.io", From b987be0b6774f6f16f81872da0f3eb257f31a3fe Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Sat, 11 Mar 2023 00:13:03 +0100 Subject: [PATCH 069/343] fixup! chore(csp): group by env, add xyz urls --- libs/constants/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/constants/index.js b/libs/constants/index.js index 34ce8886fe1a..fd8550488d3e 100644 --- a/libs/constants/index.js +++ b/libs/constants/index.js @@ -109,6 +109,7 @@ export const CSP_DIRECTIVES = { "yari-demos.prod.mdn.mozit.cloud", // Stage. + "interactive-examples.mdn.allizom.net", "interactive-examples.stage.mdn.mozilla.net", "yari-demos.stage.mdn.mozit.cloud", From 01ae957f5fef2fa444111c46117ae6f348fcf3e2 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 16 Mar 2023 17:06:03 +0100 Subject: [PATCH 070/343] feat(gcp/function): add stripe/plans api --- gcp/function/package-lock.json | 8 + gcp/function/package.json | 2 + gcp/function/src/app.ts | 2 + .../src/handlers/plans-prod-lookup.json | 3705 ++++++++++++++++ .../src/handlers/plans-stage-lookup.json | 3857 +++++++++++++++++ gcp/function/src/handlers/stripePlans.ts | 119 + 6 files changed, 7693 insertions(+) create mode 100644 gcp/function/src/handlers/plans-prod-lookup.json create mode 100644 gcp/function/src/handlers/plans-stage-lookup.json create mode 100644 gcp/function/src/handlers/stripePlans.ts diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index adbb26491e4e..831cfa2cf658 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -14,6 +14,7 @@ "@yari-internal/fundamental-redirects": "file:src/internal/fundamental-redirects", "@yari-internal/locale-utils": "file:src/internal/locale-utils", "@yari-internal/slug-utils": "file:src/internal/slug-utils", + "accept-language-parser": "^1.5.0", "dotenv": "^16.0.3", "express": "^4.18.2", "http-proxy": "^1.18.1", @@ -21,6 +22,7 @@ }, "devDependencies": { "@swc/core": "^1.3.38", + "@types/accept-language-parser": "^1.5.3", "@types/http-proxy": "^1.17.10", "npm-run-all": "^4.1.5", "ts-node": "^10.9.1", @@ -382,6 +384,12 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "node_modules/@types/accept-language-parser": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@types/accept-language-parser/-/accept-language-parser-1.5.3.tgz", + "integrity": "sha512-S8oM29O6nnRC3/+rwYV7GBYIIgNIZ52PCxqBG7OuItq9oATnYWy8FfeLKwvq5F7pIYjeeBSCI7y+l+Z9UEQpVQ==", + "dev": true + }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", diff --git a/gcp/function/package.json b/gcp/function/package.json index 022c49142c3c..f1fdc19f9816 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -23,6 +23,7 @@ "@yari-internal/fundamental-redirects": "file:src/internal/fundamental-redirects", "@yari-internal/locale-utils": "file:src/internal/locale-utils", "@yari-internal/slug-utils": "file:src/internal/slug-utils", + "accept-language-parser": "^1.5.0", "dotenv": "^16.0.3", "express": "^4.18.2", "http-proxy": "^1.18.1", @@ -30,6 +31,7 @@ }, "devDependencies": { "@swc/core": "^1.3.38", + "@types/accept-language-parser": "^1.5.3", "@types/http-proxy": "^1.17.10", "npm-run-all": "^4.1.5", "ts-node": "^10.9.1", diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index c6dbcaa6191d..3e6d2de47f6f 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -5,12 +5,14 @@ import { docs } from "./handlers/content.js"; import { client } from "./handlers/client.js"; import { bcdApi } from "./handlers/bcdApi.js"; import { rumba } from "./handlers/rumba.js"; +import { stripePlans } from "./handlers/stripePlans.js"; import { pathnameLC } from "./middlewares/pathnameLC.js"; import { contentOriginRequest } from "./middlewares/content-origin-request.js"; const mainRouter = Router(); const docsHandler = docs(); mainRouter.get("/bcd/api/*", bcdApi()); +mainRouter.all("/api/v1/stripe/plans", stripePlans); mainRouter.all("/api/*", rumba); mainRouter.all("/users/fxa/*", rumba); mainRouter.use(contentOriginRequest); diff --git a/gcp/function/src/handlers/plans-prod-lookup.json b/gcp/function/src/handlers/plans-prod-lookup.json new file mode 100644 index 000000000000..c89930fdbfe9 --- /dev/null +++ b/gcp/function/src/handlers/plans-prod-lookup.json @@ -0,0 +1,3705 @@ +{ + "countryToCurrency": { + "AS": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "CA": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "GB": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "GU": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "MP": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "MY": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "NZ": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "PR": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "SG": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "US": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "VI": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "AT": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "de" + }, + "BE": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "nl" + }, + "CY": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "el" + }, + "DE": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "de" + }, + "EE": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "et" + }, + "ES": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "es" + }, + "FI": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "en" + }, + "FR": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "fr" + }, + "GR": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "el" + }, + "IE": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "en" + }, + "IT": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "it" + }, + "LT": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "lt" + }, + "LU": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "fr" + }, + "LV": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "lv" + }, + "MT": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "en" + }, + "NL": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "nl" + }, + "SE": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "en" + }, + "SK": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "sk" + }, + "SI": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "sl" + }, + "PT": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "pt" + }, + "CH": { + "currency": "chf", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + } + }, + "defaultLanguage": "de" + } + }, + "langCurrencyToPlans": { + "usd-en": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "usd", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - USD EN", + "lookup_key": "usd-en-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:720bc80adfa6988d": "mdn_plus_5m", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KeG02JNcmPzuWtR1oBrw8o6" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "usd", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - USD EN", + "lookup_key": "usd-en-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:720bc80adfa6988d": "mdn_plus_5y", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KeG02JNcmPzuWtRslZijhQu" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "usd", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - USD EN", + "lookup_key": "usd-en-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:720bc80adfa6988d": "mdn_plus_10m", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KeG02JNcmPzuWtRuAnIgNHh" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "usd", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - USD EN", + "lookup_key": "usd-en-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:720bc80adfa6988d": "mdn_plus_10y", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KeG02JNcmPzuWtRlrSiLTI6" + } + }, + "eur-en": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR EN", + "lookup_key": "eur-en-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:720bc80adfa6988d": "mdn_plus_5m", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeX9JNcmPzuWtRJNelT86c" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR EN", + "lookup_key": "eur-en-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:720bc80adfa6988d": "mdn_plus_5y", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeX9JNcmPzuWtR4dOStCqA" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR EN", + "lookup_key": "eur-en-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:720bc80adfa6988d": "mdn_plus_10m", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXAJNcmPzuWtRjvfOVIUP" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR EN", + "lookup_key": "eur-en-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:720bc80adfa6988d": "mdn_plus_10y", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXAJNcmPzuWtR5hwpwsUr" + } + }, + "eur-de": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR DE", + "lookup_key": "eur-de-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:720bc80adfa6988d": "mdn_plus_5m", + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXBJNcmPzuWtR7opydXog" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR DE", + "lookup_key": "eur-de-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:720bc80adfa6988d": "mdn_plus_5y", + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXBJNcmPzuWtRBSY6DGlv" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR DE", + "lookup_key": "eur-de-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:720bc80adfa6988d": "mdn_plus_10m", + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXBJNcmPzuWtRNWzQ6IX4" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR DE", + "lookup_key": "eur-de-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:720bc80adfa6988d": "mdn_plus_10y", + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXCJNcmPzuWtR5fl7C1tx" + } + }, + "eur-el": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR EL", + "lookup_key": "eur-el-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:720bc80adfa6988d": "mdn_plus_5m", + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "price_id": "price_1L5915JNcmPzuWtRMXzNAj50" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR EL", + "lookup_key": "eur-el-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:720bc80adfa6988d": "mdn_plus_5y", + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "price_id": "price_1L5916JNcmPzuWtRdYKhF10o" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR EL", + "lookup_key": "eur-el-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:720bc80adfa6988d": "mdn_plus_10m", + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "price_id": "price_1L5916JNcmPzuWtRkCVnoV9Y" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR EL", + "lookup_key": "eur-el-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:720bc80adfa6988d": "mdn_plus_10y", + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "price_id": "price_1L5917JNcmPzuWtRGcDOMndA" + } + }, + "eur-es": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR ES", + "lookup_key": "eur-es-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:720bc80adfa6988d": "mdn_plus_5m", + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "price_id": "price_1KqeXCJNcmPzuWtR05tPDWqA" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR ES", + "lookup_key": "eur-es-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:720bc80adfa6988d": "mdn_plus_5y", + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "price_id": "price_1KqeXCJNcmPzuWtR5yy0wlfO" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR ES", + "lookup_key": "eur-es-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:720bc80adfa6988d": "mdn_plus_10m", + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "price_id": "price_1KqeXDJNcmPzuWtRJAstOkqA" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR ES", + "lookup_key": "eur-es-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:720bc80adfa6988d": "mdn_plus_10y", + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "price_id": "price_1KqeXDJNcmPzuWtR8hPdxJiS" + } + }, + "eur-et": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR ET", + "lookup_key": "eur-et-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:720bc80adfa6988d": "mdn_plus_5m", + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "price_id": "price_1L5917JNcmPzuWtRXU88NcRx" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR ET", + "lookup_key": "eur-et-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:720bc80adfa6988d": "mdn_plus_5y", + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "price_id": "price_1L5918JNcmPzuWtRYHCMLCWR" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR ET", + "lookup_key": "eur-et-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:720bc80adfa6988d": "mdn_plus_10m", + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "price_id": "price_1L5918JNcmPzuWtRkS2tNVZC" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR ET", + "lookup_key": "eur-et-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:720bc80adfa6988d": "mdn_plus_10y", + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "price_id": "price_1L5918JNcmPzuWtRl47Ws8m0" + } + }, + "eur-fr": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR FR", + "lookup_key": "eur-fr-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:720bc80adfa6988d": "mdn_plus_5m", + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "price_id": "price_1KqeXDJNcmPzuWtRoL9NNeK4" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR FR", + "lookup_key": "eur-fr-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:720bc80adfa6988d": "mdn_plus_5y", + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "price_id": "price_1KqeXEJNcmPzuWtRTw1w8bX5" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR FR", + "lookup_key": "eur-fr-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:720bc80adfa6988d": "mdn_plus_10m", + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "price_id": "price_1KqeXEJNcmPzuWtR3WZLuJ6K" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR FR", + "lookup_key": "eur-fr-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:720bc80adfa6988d": "mdn_plus_10y", + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "price_id": "price_1KqeXEJNcmPzuWtR1kOfChz0" + } + }, + "eur-it": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR IT", + "lookup_key": "eur-it-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:720bc80adfa6988d": "mdn_plus_5m", + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXFJNcmPzuWtRUBiVlTVX" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR IT", + "lookup_key": "eur-it-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:720bc80adfa6988d": "mdn_plus_5y", + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXFJNcmPzuWtRjdDWnMU6" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR IT", + "lookup_key": "eur-it-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:720bc80adfa6988d": "mdn_plus_10m", + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXFJNcmPzuWtR2UJ1TVSG" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR IT", + "lookup_key": "eur-it-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:720bc80adfa6988d": "mdn_plus_10y", + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXGJNcmPzuWtR7cw3rh90" + } + }, + "eur-lt": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR LT", + "lookup_key": "eur-lt-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:720bc80adfa6988d": "mdn_plus_5m", + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "price_id": "price_1L5919JNcmPzuWtRvhirUtKK" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR LT", + "lookup_key": "eur-lt-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:720bc80adfa6988d": "mdn_plus_5y", + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "price_id": "price_1L5919JNcmPzuWtRNlOkp6pJ" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR LT", + "lookup_key": "eur-lt-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:720bc80adfa6988d": "mdn_plus_10m", + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "price_id": "price_1L591AJNcmPzuWtR0YIkMvZC" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR LT", + "lookup_key": "eur-lt-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:720bc80adfa6988d": "mdn_plus_10y", + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "price_id": "price_1L591AJNcmPzuWtRK2eZPpI9" + } + }, + "eur-lv": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR LV", + "lookup_key": "eur-lv-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:720bc80adfa6988d": "mdn_plus_5m", + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "price_id": "price_1L591AJNcmPzuWtRNotb24QG" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR LV", + "lookup_key": "eur-lv-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:720bc80adfa6988d": "mdn_plus_5y", + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "price_id": "price_1L591BJNcmPzuWtRS32AB1gs" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR LV", + "lookup_key": "eur-lv-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:720bc80adfa6988d": "mdn_plus_10m", + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "price_id": "price_1L591BJNcmPzuWtRk7skMZRp" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR LV", + "lookup_key": "eur-lv-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:720bc80adfa6988d": "mdn_plus_10y", + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "price_id": "price_1L591CJNcmPzuWtRUUa1ybJs" + } + }, + "eur-nl": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR NL", + "lookup_key": "eur-nl-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:720bc80adfa6988d": "mdn_plus_5m", + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXGJNcmPzuWtRcjH3vbwC" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR NL", + "lookup_key": "eur-nl-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:720bc80adfa6988d": "mdn_plus_5y", + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXGJNcmPzuWtRbycawcNr" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR NL", + "lookup_key": "eur-nl-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:720bc80adfa6988d": "mdn_plus_10m", + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXHJNcmPzuWtRxV9W8tIQ" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR NL", + "lookup_key": "eur-nl-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:720bc80adfa6988d": "mdn_plus_10y", + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXHJNcmPzuWtR9IvbHgFI" + } + }, + "eur-pt": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR PT", + "lookup_key": "eur-pt-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:720bc80adfa6988d": "mdn_plus_5m", + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "price_id": "price_1L591CJNcmPzuWtRhzqtQqRW" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR PT", + "lookup_key": "eur-pt-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:720bc80adfa6988d": "mdn_plus_5y", + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "price_id": "price_1L591CJNcmPzuWtRkmi7uEDk" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR PT", + "lookup_key": "eur-pt-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:720bc80adfa6988d": "mdn_plus_10m", + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "price_id": "price_1L591DJNcmPzuWtRQH8Mybz5" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR PT", + "lookup_key": "eur-pt-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:720bc80adfa6988d": "mdn_plus_10y", + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "price_id": "price_1L591DJNcmPzuWtRFZNp49Py" + } + }, + "eur-sk": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR SK", + "lookup_key": "eur-sk-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:720bc80adfa6988d": "mdn_plus_5m", + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "price_id": "price_1L591DJNcmPzuWtRdBWgbH2y" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR SK", + "lookup_key": "eur-sk-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:720bc80adfa6988d": "mdn_plus_5y", + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "price_id": "price_1L591EJNcmPzuWtRL5J0OPcy" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR SK", + "lookup_key": "eur-sk-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:720bc80adfa6988d": "mdn_plus_10m", + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "price_id": "price_1L591EJNcmPzuWtRKLalyUPd" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR SK", + "lookup_key": "eur-sk-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:720bc80adfa6988d": "mdn_plus_10y", + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "price_id": "price_1L591EJNcmPzuWtRwJobTR51" + } + }, + "eur-sl": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR SL", + "lookup_key": "eur-sl-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:720bc80adfa6988d": "mdn_plus_5m", + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "price_id": "price_1L591FJNcmPzuWtRIuDMHIsn" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR SL", + "lookup_key": "eur-sl-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:720bc80adfa6988d": "mdn_plus_5y", + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "price_id": "price_1L591FJNcmPzuWtRAa0tq6bw" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR SL", + "lookup_key": "eur-sl-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:720bc80adfa6988d": "mdn_plus_10m", + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "price_id": "price_1L591FJNcmPzuWtRduAZLny6" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR SL", + "lookup_key": "eur-sl-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:720bc80adfa6988d": "mdn_plus_10y", + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "price_id": "price_1L591GJNcmPzuWtRilCry5VT" + } + }, + "eur-tr": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR TR", + "lookup_key": "eur-tr-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:720bc80adfa6988d": "mdn_plus_5m", + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + }, + "price_id": "price_1L591GJNcmPzuWtRLDAVfALF" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR TR", + "lookup_key": "eur-tr-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:720bc80adfa6988d": "mdn_plus_5y", + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + }, + "price_id": "price_1L591GJNcmPzuWtRU3dE0gMp" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR TR", + "lookup_key": "eur-tr-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:720bc80adfa6988d": "mdn_plus_10m", + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + }, + "price_id": "price_1L591HJNcmPzuWtRCh2COINB" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR TR", + "lookup_key": "eur-tr-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:720bc80adfa6988d": "mdn_plus_10y", + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + }, + "price_id": "price_1L591HJNcmPzuWtR6xBbf4ah" + } + }, + "chf-en": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "chf", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - CHF EN", + "lookup_key": "chf-en-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:720bc80adfa6988d": "mdn_plus_5m", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXHJNcmPzuWtR4itTRF7Y" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "chf", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - CHF EN", + "lookup_key": "chf-en-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:720bc80adfa6988d": "mdn_plus_5y", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXIJNcmPzuWtRZJlZhXpW" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "chf", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - CHF EN", + "lookup_key": "chf-en-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:720bc80adfa6988d": "mdn_plus_10m", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXIJNcmPzuWtRKwoSofy2" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "chf", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - CHF EN", + "lookup_key": "chf-en-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:720bc80adfa6988d": "mdn_plus_10y", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXJJNcmPzuWtRDSDHSlAl" + } + }, + "chf-de": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "chf", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - CHF DE", + "lookup_key": "chf-de-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:720bc80adfa6988d": "mdn_plus_5m", + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXJJNcmPzuWtRDFlszhVO" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "chf", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - CHF DE", + "lookup_key": "chf-de-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:720bc80adfa6988d": "mdn_plus_5y", + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXKJNcmPzuWtREcYZdrje" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "chf", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - CHF DE", + "lookup_key": "chf-de-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:720bc80adfa6988d": "mdn_plus_10m", + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXKJNcmPzuWtRExRB58k0" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "chf", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - CHF DE", + "lookup_key": "chf-de-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:720bc80adfa6988d": "mdn_plus_10y", + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXLJNcmPzuWtRtypFouG4" + } + }, + "chf-fr": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "chf", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - CHF FR", + "lookup_key": "chf-fr-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:720bc80adfa6988d": "mdn_plus_5m", + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "price_id": "price_1KqeXLJNcmPzuWtRyS2uTKyE" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "chf", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - CHF FR", + "lookup_key": "chf-fr-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:720bc80adfa6988d": "mdn_plus_5y", + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "price_id": "price_1KqeXLJNcmPzuWtRbH6wD6sm" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "chf", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - CHF FR", + "lookup_key": "chf-fr-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:720bc80adfa6988d": "mdn_plus_10m", + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "price_id": "price_1KqeXMJNcmPzuWtR46VuMNtb" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "chf", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - CHF FR", + "lookup_key": "chf-fr-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:720bc80adfa6988d": "mdn_plus_10y", + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "price_id": "price_1KqeXMJNcmPzuWtR3bNmxM4C" + } + }, + "chf-it": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "chf", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - CHF IT", + "lookup_key": "chf-it-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:720bc80adfa6988d": "mdn_plus_5m", + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXMJNcmPzuWtRC7IJcwUD" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "chf", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - CHF IT", + "lookup_key": "chf-it-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:720bc80adfa6988d": "mdn_plus_5y", + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXNJNcmPzuWtRSCJ2GbVn" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "chf", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - CHF IT", + "lookup_key": "chf-it-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:720bc80adfa6988d": "mdn_plus_10m", + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXNJNcmPzuWtRRb5z07dQ" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "chf", + "product": "prod_LKvr8fYGbBxcaZ", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - CHF IT", + "lookup_key": "chf-it-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:720bc80adfa6988d": "mdn_plus_10y", + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqeXOJNcmPzuWtRMBae7104" + } + } + } +} diff --git a/gcp/function/src/handlers/plans-stage-lookup.json b/gcp/function/src/handlers/plans-stage-lookup.json new file mode 100644 index 000000000000..910518716518 --- /dev/null +++ b/gcp/function/src/handlers/plans-stage-lookup.json @@ -0,0 +1,3857 @@ +{ + "countryToCurrency": { + "AS": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "CA": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "GB": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "GU": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "MP": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "MY": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "NZ": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "PR": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "SG": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "US": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "VI": { + "currency": "usd", + "supportedLanguages": { + "en": null + }, + "defaultLanguage": "en" + }, + "AT": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "de" + }, + "BE": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "nl" + }, + "CY": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "el" + }, + "DE": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "de" + }, + "EE": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "et" + }, + "ES": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "es" + }, + "FI": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "en" + }, + "FR": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "fr" + }, + "GR": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "el" + }, + "IE": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "en" + }, + "IT": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "it" + }, + "LT": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "lt" + }, + "LU": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "fr" + }, + "LV": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "lv" + }, + "MT": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "en" + }, + "NL": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "nl" + }, + "SE": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "en" + }, + "SK": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "sk" + }, + "SI": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "sl" + }, + "PT": { + "currency": "eur", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "el": { + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "es": { + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "et": { + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "lt": { + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "lv": { + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "nl": { + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "pt": { + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "sk": { + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "sl": { + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "tr": { + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + } + }, + "defaultLanguage": "pt" + }, + "CH": { + "currency": "chf", + "supportedLanguages": { + "en": null, + "de": { + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "fr": { + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "it": { + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + } + }, + "defaultLanguage": "de" + } + }, + "langCurrencyToPlans": { + "usd-en": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "usd", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - USD EN", + "lookup_key": "usd-en-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:b6f5727337fefe8e": "mdn_plus_5m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", + "capabilities:ed18cbc69ec23491": "mdn_plus_5m", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1JFoTYKb9q6OnNsLalexa03p" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "usd", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - USD EN", + "lookup_key": "usd-en-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:b6f5727337fefe8e": "mdn_plus_5y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", + "capabilities:ed18cbc69ec23491": "mdn_plus_5y", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1JpIPwKb9q6OnNsLJLsIqMp7" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "usd", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - USD EN", + "lookup_key": "usd-en-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:b6f5727337fefe8e": "mdn_plus_10m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", + "capabilities:ed18cbc69ec23491": "mdn_plus_10m", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1K6X7gKb9q6OnNsLi44HdLcC" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "usd", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - USD EN", + "lookup_key": "usd-en-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:b6f5727337fefe8e": "mdn_plus_10y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", + "capabilities:ed18cbc69ec23491": "mdn_plus_10y", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1K6X8VKb9q6OnNsLFlUcEiu4" + } + }, + "eur-en": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR EN", + "lookup_key": "eur-en-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:b6f5727337fefe8e": "mdn_plus_5m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", + "capabilities:ed18cbc69ec23491": "mdn_plus_5m", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqdudKb9q6OnNsLvZkMKGkx" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR EN", + "lookup_key": "eur-en-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:b6f5727337fefe8e": "mdn_plus_5y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", + "capabilities:ed18cbc69ec23491": "mdn_plus_5y", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqdudKb9q6OnNsLJAJVh0rh" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR EN", + "lookup_key": "eur-en-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:b6f5727337fefe8e": "mdn_plus_10m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", + "capabilities:ed18cbc69ec23491": "mdn_plus_10m", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqdudKb9q6OnNsLDYhWPIjT" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR EN", + "lookup_key": "eur-en-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:b6f5727337fefe8e": "mdn_plus_10y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", + "capabilities:ed18cbc69ec23491": "mdn_plus_10y", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqdueKb9q6OnNsLDg4Toybn" + } + }, + "eur-de": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR DE", + "lookup_key": "eur-de-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:b6f5727337fefe8e": "mdn_plus_5m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", + "capabilities:ed18cbc69ec23491": "mdn_plus_5m", + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "price_id": "price_1Ko6oDKb9q6OnNsL3UV65T60" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR DE", + "lookup_key": "eur-de-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:b6f5727337fefe8e": "mdn_plus_5y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", + "capabilities:ed18cbc69ec23491": "mdn_plus_5y", + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "price_id": "price_1Ko6qAKb9q6OnNsLdsHFRRYW" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR DE", + "lookup_key": "eur-de-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:b6f5727337fefe8e": "mdn_plus_10m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", + "capabilities:ed18cbc69ec23491": "mdn_plus_10m", + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "price_id": "price_1Ko6rsKb9q6OnNsL9jMzlpUn" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR DE", + "lookup_key": "eur-de-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:b6f5727337fefe8e": "mdn_plus_10y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", + "capabilities:ed18cbc69ec23491": "mdn_plus_10y", + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "price_id": "price_1Ko6stKb9q6OnNsL4rnrw4Wn" + } + }, + "eur-el": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR EL", + "lookup_key": "eur-el-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:b6f5727337fefe8e": "mdn_plus_5m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", + "capabilities:ed18cbc69ec23491": "mdn_plus_5m", + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "price_id": "price_1L2tOPKb9q6OnNsLvZalzbLg" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR EL", + "lookup_key": "eur-el-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:b6f5727337fefe8e": "mdn_plus_5y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", + "capabilities:ed18cbc69ec23491": "mdn_plus_5y", + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "price_id": "price_1L2tOPKb9q6OnNsLkpUbrnJb" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR EL", + "lookup_key": "eur-el-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:b6f5727337fefe8e": "mdn_plus_10m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", + "capabilities:ed18cbc69ec23491": "mdn_plus_10m", + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "price_id": "price_1L2tOPKb9q6OnNsLvH7CfRM9" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR EL", + "lookup_key": "eur-el-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:b6f5727337fefe8e": "mdn_plus_10y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", + "capabilities:ed18cbc69ec23491": "mdn_plus_10y", + "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", + "product:subtitle": "Ειδοποιήσεις σελίδας", + "product:details:1": "Συλλογή άρθρων", + "product:details:2": "MDN εκτός σύνδεσης", + "product:details:3": "Συνέχεια σε MDN Plus" + }, + "price_id": "price_1L2tOQKb9q6OnNsLep9UMq1P" + } + }, + "eur-es": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR ES", + "lookup_key": "eur-es-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:b6f5727337fefe8e": "mdn_plus_5m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", + "capabilities:ed18cbc69ec23491": "mdn_plus_5m", + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "price_id": "price_1KqdugKb9q6OnNsL41ATLMv6" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR ES", + "lookup_key": "eur-es-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:b6f5727337fefe8e": "mdn_plus_5y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", + "capabilities:ed18cbc69ec23491": "mdn_plus_5y", + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "price_id": "price_1KqdugKb9q6OnNsLXv0GwhaT" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR ES", + "lookup_key": "eur-es-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:b6f5727337fefe8e": "mdn_plus_10m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", + "capabilities:ed18cbc69ec23491": "mdn_plus_10m", + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "price_id": "price_1KqdugKb9q6OnNsLF5ZHUiBP" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR ES", + "lookup_key": "eur-es-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:b6f5727337fefe8e": "mdn_plus_10y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", + "capabilities:ed18cbc69ec23491": "mdn_plus_10y", + "product:successActionButtonLabel": "Continuar a MDN Plus", + "product:subtitle": "Un servicio MDN prémium", + "product:details:1": "Notificaciones de página", + "product:details:2": "Colecciones de artículos", + "product:details:3": "MDN disponible sin conexión" + }, + "price_id": "price_1KqduhKb9q6OnNsLfrNPlNDh" + } + }, + "eur-et": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR ET", + "lookup_key": "eur-et-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:b6f5727337fefe8e": "mdn_plus_5m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", + "capabilities:ed18cbc69ec23491": "mdn_plus_5m", + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "price_id": "price_1L2tOQKb9q6OnNsLYmRXAHtz" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR ET", + "lookup_key": "eur-et-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:b6f5727337fefe8e": "mdn_plus_5y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", + "capabilities:ed18cbc69ec23491": "mdn_plus_5y", + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "price_id": "price_1L2tORKb9q6OnNsLhVlLhMsM" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR ET", + "lookup_key": "eur-et-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:b6f5727337fefe8e": "mdn_plus_10m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", + "capabilities:ed18cbc69ec23491": "mdn_plus_10m", + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "price_id": "price_1L2tORKb9q6OnNsLEqTNWbxw" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR ET", + "lookup_key": "eur-et-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:b6f5727337fefe8e": "mdn_plus_10y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", + "capabilities:ed18cbc69ec23491": "mdn_plus_10y", + "product:successActionButtonLabel": "MDN Premium Service", + "product:subtitle": "Lehe teavitused", + "product:details:1": "Artiklite kollektsioonid", + "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", + "product:details:3": "MDN offline" + }, + "price_id": "price_1L2tOSKb9q6OnNsLdujiVlew" + } + }, + "eur-fr": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR FR", + "lookup_key": "eur-fr-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:b6f5727337fefe8e": "mdn_plus_5m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", + "capabilities:ed18cbc69ec23491": "mdn_plus_5m", + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "price_id": "price_1KqduhKb9q6OnNsLOPbGN60q" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR FR", + "lookup_key": "eur-fr-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:b6f5727337fefe8e": "mdn_plus_5y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", + "capabilities:ed18cbc69ec23491": "mdn_plus_5y", + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "price_id": "price_1KqduhKb9q6OnNsL2Iio3oSP" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR FR", + "lookup_key": "eur-fr-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:b6f5727337fefe8e": "mdn_plus_10m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", + "capabilities:ed18cbc69ec23491": "mdn_plus_10m", + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "price_id": "price_1KqduiKb9q6OnNsLzF5Ca5LV" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR FR", + "lookup_key": "eur-fr-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:b6f5727337fefe8e": "mdn_plus_10y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", + "capabilities:ed18cbc69ec23491": "mdn_plus_10y", + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "price_id": "price_1KqduiKb9q6OnNsLH0G5Lmvk" + } + }, + "eur-it": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR IT", + "lookup_key": "eur-it-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:b6f5727337fefe8e": "mdn_plus_5m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", + "capabilities:ed18cbc69ec23491": "mdn_plus_5m", + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqduiKb9q6OnNsL6Amq5kOo" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR IT", + "lookup_key": "eur-it-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:b6f5727337fefe8e": "mdn_plus_5y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", + "capabilities:ed18cbc69ec23491": "mdn_plus_5y", + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqdujKb9q6OnNsLhdfctEHd" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR IT", + "lookup_key": "eur-it-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:b6f5727337fefe8e": "mdn_plus_10m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", + "capabilities:ed18cbc69ec23491": "mdn_plus_10m", + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqdujKb9q6OnNsLPFNYmZdu" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR IT", + "lookup_key": "eur-it-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:b6f5727337fefe8e": "mdn_plus_10y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", + "capabilities:ed18cbc69ec23491": "mdn_plus_10y", + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqdukKb9q6OnNsLje4nw89h" + } + }, + "eur-lt": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR LT", + "lookup_key": "eur-lt-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:b6f5727337fefe8e": "mdn_plus_5m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", + "capabilities:ed18cbc69ec23491": "mdn_plus_5m", + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "price_id": "price_1L2tOSKb9q6OnNsLcj6DVCay" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR LT", + "lookup_key": "eur-lt-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:b6f5727337fefe8e": "mdn_plus_5y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", + "capabilities:ed18cbc69ec23491": "mdn_plus_5y", + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "price_id": "price_1L2tOSKb9q6OnNsLMXAdyweB" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR LT", + "lookup_key": "eur-lt-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:b6f5727337fefe8e": "mdn_plus_10m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", + "capabilities:ed18cbc69ec23491": "mdn_plus_10m", + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "price_id": "price_1L2tOTKb9q6OnNsLeLloEB9W" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR LT", + "lookup_key": "eur-lt-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:b6f5727337fefe8e": "mdn_plus_10y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", + "capabilities:ed18cbc69ec23491": "mdn_plus_10y", + "product:successActionButtonLabel": "MDN „Premium“ paslauga", + "product:subtitle": "Puslapio pranešimai", + "product:details:1": "Straipsnių rinkiniai", + "product:details:2": "Tęsti su „MDN Plus“", + "product:details:3": "MDN neprisijungus" + }, + "price_id": "price_1L2tOTKb9q6OnNsLtyHtZTZs" + } + }, + "eur-lv": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR LV", + "lookup_key": "eur-lv-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:b6f5727337fefe8e": "mdn_plus_5m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", + "capabilities:ed18cbc69ec23491": "mdn_plus_5m", + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "price_id": "price_1L2tOTKb9q6OnNsLavSzI6Rc" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR LV", + "lookup_key": "eur-lv-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:b6f5727337fefe8e": "mdn_plus_5y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", + "capabilities:ed18cbc69ec23491": "mdn_plus_5y", + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "price_id": "price_1L2tOUKb9q6OnNsLkzLwkTdw" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR LV", + "lookup_key": "eur-lv-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:b6f5727337fefe8e": "mdn_plus_10m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", + "capabilities:ed18cbc69ec23491": "mdn_plus_10m", + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "price_id": "price_1L2tOUKb9q6OnNsLLJLA6cln" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR LV", + "lookup_key": "eur-lv-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:b6f5727337fefe8e": "mdn_plus_10y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", + "capabilities:ed18cbc69ec23491": "mdn_plus_10y", + "product:successActionButtonLabel": "MDN Premium pakalpojums", + "product:subtitle": "Lapas paziņojumi", + "product:details:1": "Rakstu apkopojumi", + "product:details:2": "Turpināt ar MDN Plus", + "product:details:3": "MDN bezsaistē" + }, + "price_id": "price_1L2tOUKb9q6OnNsLV6ORmdfX" + } + }, + "eur-nl": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR NL", + "lookup_key": "eur-nl-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:b6f5727337fefe8e": "mdn_plus_5m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", + "capabilities:ed18cbc69ec23491": "mdn_plus_5m", + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqdukKb9q6OnNsLjjA8mTpM" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR NL", + "lookup_key": "eur-nl-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:b6f5727337fefe8e": "mdn_plus_5y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", + "capabilities:ed18cbc69ec23491": "mdn_plus_5y", + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqdukKb9q6OnNsLPCtBqPcw" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR NL", + "lookup_key": "eur-nl-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:b6f5727337fefe8e": "mdn_plus_10m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", + "capabilities:ed18cbc69ec23491": "mdn_plus_10m", + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqdulKb9q6OnNsLWgRvCo2X" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR NL", + "lookup_key": "eur-nl-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:b6f5727337fefe8e": "mdn_plus_10y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", + "capabilities:ed18cbc69ec23491": "mdn_plus_10y", + "product:successActionButtonLabel": "Doorgaan naar MDN Plus", + "product:subtitle": "Een MDN Premium-service", + "product:details:1": "Paginameldingen", + "product:details:2": "Verzamelingen artikelen", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqdulKb9q6OnNsLnNtjAc1b" + } + }, + "eur-pt": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR PT", + "lookup_key": "eur-pt-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:b6f5727337fefe8e": "mdn_plus_5m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", + "capabilities:ed18cbc69ec23491": "mdn_plus_5m", + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "price_id": "price_1L2tOVKb9q6OnNsLKEZeiAUa" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR PT", + "lookup_key": "eur-pt-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:b6f5727337fefe8e": "mdn_plus_5y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", + "capabilities:ed18cbc69ec23491": "mdn_plus_5y", + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "price_id": "price_1L2tOVKb9q6OnNsLlL79fx7O" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR PT", + "lookup_key": "eur-pt-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:b6f5727337fefe8e": "mdn_plus_10m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", + "capabilities:ed18cbc69ec23491": "mdn_plus_10m", + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "price_id": "price_1L2tOWKb9q6OnNsLlvcFItux" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR PT", + "lookup_key": "eur-pt-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:b6f5727337fefe8e": "mdn_plus_10y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", + "capabilities:ed18cbc69ec23491": "mdn_plus_10y", + "product:successActionButtonLabel": "Um Serviço Premium MDN", + "product:subtitle": "Notificações de página", + "product:details:1": "Coleções de artigos", + "product:details:2": "Continuar para o MDN Plus", + "product:details:3": "MDN Offline" + }, + "price_id": "price_1L2tOWKb9q6OnNsLeDlMYfzM" + } + }, + "eur-sk": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR SK", + "lookup_key": "eur-sk-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:b6f5727337fefe8e": "mdn_plus_5m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", + "capabilities:ed18cbc69ec23491": "mdn_plus_5m", + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "price_id": "price_1L2tOXKb9q6OnNsLgdMHi7aT" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR SK", + "lookup_key": "eur-sk-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:b6f5727337fefe8e": "mdn_plus_5y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", + "capabilities:ed18cbc69ec23491": "mdn_plus_5y", + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "price_id": "price_1L2tOXKb9q6OnNsLY1NOhF8J" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR SK", + "lookup_key": "eur-sk-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:b6f5727337fefe8e": "mdn_plus_10m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", + "capabilities:ed18cbc69ec23491": "mdn_plus_10m", + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "price_id": "price_1L2tOXKb9q6OnNsL0USQVLnk" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR SK", + "lookup_key": "eur-sk-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:b6f5727337fefe8e": "mdn_plus_10y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", + "capabilities:ed18cbc69ec23491": "mdn_plus_10y", + "product:successActionButtonLabel": "Služba MDN Premium", + "product:subtitle": "Upozornenia na stránke", + "product:details:1": "Zbierky článkov", + "product:details:2": "Pokračujte na MDN Plus", + "product:details:3": "MDN offline" + }, + "price_id": "price_1L2tOYKb9q6OnNsLLN78izjz" + } + }, + "eur-sl": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR SL", + "lookup_key": "eur-sl-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:b6f5727337fefe8e": "mdn_plus_5m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", + "capabilities:ed18cbc69ec23491": "mdn_plus_5m", + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "price_id": "price_1L2tOYKb9q6OnNsL0FTDUmNL" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR SL", + "lookup_key": "eur-sl-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:b6f5727337fefe8e": "mdn_plus_5y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", + "capabilities:ed18cbc69ec23491": "mdn_plus_5y", + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "price_id": "price_1L2tOZKb9q6OnNsLVUOKdstR" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR SL", + "lookup_key": "eur-sl-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:b6f5727337fefe8e": "mdn_plus_10m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", + "capabilities:ed18cbc69ec23491": "mdn_plus_10m", + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "price_id": "price_1L2tOZKb9q6OnNsL73fEBYE0" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR SL", + "lookup_key": "eur-sl-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:b6f5727337fefe8e": "mdn_plus_10y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", + "capabilities:ed18cbc69ec23491": "mdn_plus_10y", + "product:successActionButtonLabel": "Storitev MDN Premium", + "product:subtitle": "Obvestila strani", + "product:details:1": "Zbirke člankov", + "product:details:2": "Nadaljuj na MDN Plus", + "product:details:3": "MDN brez povezave" + }, + "price_id": "price_1L2tOaKb9q6OnNsLroZYuVLp" + } + }, + "eur-tr": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - EUR TR", + "lookup_key": "eur-tr-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:b6f5727337fefe8e": "mdn_plus_5m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", + "capabilities:ed18cbc69ec23491": "mdn_plus_5m", + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + }, + "price_id": "price_1L2tOaKb9q6OnNsLawGUUtNl" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - EUR TR", + "lookup_key": "eur-tr-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:b6f5727337fefe8e": "mdn_plus_5y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", + "capabilities:ed18cbc69ec23491": "mdn_plus_5y", + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + }, + "price_id": "price_1L2tOaKb9q6OnNsL3LhuZ3lY" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - EUR TR", + "lookup_key": "eur-tr-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:b6f5727337fefe8e": "mdn_plus_10m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", + "capabilities:ed18cbc69ec23491": "mdn_plus_10m", + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + }, + "price_id": "price_1L2tObKb9q6OnNsLtuAwE66M" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "eur", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - EUR TR", + "lookup_key": "eur-tr-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:b6f5727337fefe8e": "mdn_plus_10y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", + "capabilities:ed18cbc69ec23491": "mdn_plus_10y", + "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", + "product:subtitle": "Sayfa bildirimleri", + "product:details:1": "Toplanmış makaleler", + "product:details:2": "MDN Plus'a devam edin", + "product:details:3": "MDN çevrim dışı" + }, + "price_id": "price_1L2tObKb9q6OnNsLdE48ccAa" + } + }, + "chf-en": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "chf", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - CHF EN", + "lookup_key": "chf-en-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:b6f5727337fefe8e": "mdn_plus_5m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", + "capabilities:ed18cbc69ec23491": "mdn_plus_5m", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqdulKb9q6OnNsLbkK1T4dR" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "chf", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - CHF EN", + "lookup_key": "chf-en-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:b6f5727337fefe8e": "mdn_plus_5y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", + "capabilities:ed18cbc69ec23491": "mdn_plus_5y", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqdumKb9q6OnNsLfjFLaTzi" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "chf", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - CHF EN", + "lookup_key": "chf-en-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:b6f5727337fefe8e": "mdn_plus_10m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", + "capabilities:ed18cbc69ec23491": "mdn_plus_10m", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqdumKb9q6OnNsLc248nWtE" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "chf", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - CHF EN", + "lookup_key": "chf-en-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:b6f5727337fefe8e": "mdn_plus_10y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", + "capabilities:ed18cbc69ec23491": "mdn_plus_10y", + "product:successActionButtonLabel": "Continue to MDN Plus", + "product:subtitle": "An MDN Premium Service", + "product:details:1": "Page notifications", + "product:details:2": "Collections of articles", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqdumKb9q6OnNsLEbRL4nAo" + } + }, + "chf-de": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "chf", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - CHF DE", + "lookup_key": "chf-de-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:b6f5727337fefe8e": "mdn_plus_5m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", + "capabilities:ed18cbc69ec23491": "mdn_plus_5m", + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqdunKb9q6OnNsLMgnnIma0" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "chf", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - CHF DE", + "lookup_key": "chf-de-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:b6f5727337fefe8e": "mdn_plus_5y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", + "capabilities:ed18cbc69ec23491": "mdn_plus_5y", + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqdunKb9q6OnNsLiF9qvEVf" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "chf", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - CHF DE", + "lookup_key": "chf-de-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:b6f5727337fefe8e": "mdn_plus_10m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", + "capabilities:ed18cbc69ec23491": "mdn_plus_10m", + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqdunKb9q6OnNsLnaEHicOP" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "chf", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - CHF DE", + "lookup_key": "chf-de-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:b6f5727337fefe8e": "mdn_plus_10y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", + "capabilities:ed18cbc69ec23491": "mdn_plus_10y", + "product:successActionButtonLabel": "Weiter zu MDN Plus", + "product:subtitle": "Ein MDN-Premium-Dienst", + "product:details:1": "Seitenbenachrichtigungen", + "product:details:2": "Artikelsammlung", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqduoKb9q6OnNsLQrxviGu0" + } + }, + "chf-fr": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "chf", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - CHF FR", + "lookup_key": "chf-fr-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:b6f5727337fefe8e": "mdn_plus_5m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", + "capabilities:ed18cbc69ec23491": "mdn_plus_5m", + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "price_id": "price_1KqduoKb9q6OnNsLv4AoSQJg" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "chf", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - CHF FR", + "lookup_key": "chf-fr-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:b6f5727337fefe8e": "mdn_plus_5y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", + "capabilities:ed18cbc69ec23491": "mdn_plus_5y", + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "price_id": "price_1KqduoKb9q6OnNsLf9iSMYIf" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "chf", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - CHF FR", + "lookup_key": "chf-fr-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:b6f5727337fefe8e": "mdn_plus_10m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", + "capabilities:ed18cbc69ec23491": "mdn_plus_10m", + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "price_id": "price_1KqdupKb9q6OnNsLWiJvcYUp" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "chf", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - CHF FR", + "lookup_key": "chf-fr-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:b6f5727337fefe8e": "mdn_plus_10y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", + "capabilities:ed18cbc69ec23491": "mdn_plus_10y", + "product:successActionButtonLabel": "Continuer vers MDN Plus", + "product:subtitle": "Un service MDN Premium", + "product:details:1": "Notifications de la page", + "product:details:2": "Collections d’articles", + "product:details:3": "MDN hors ligne" + }, + "price_id": "price_1KqdupKb9q6OnNsLcVJfbhL9" + } + }, + "chf-it": { + "mdn_plus_5m": { + "unit_amount": 500, + "currency": "chf", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 5m - CHF IT", + "lookup_key": "chf-it-5m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "1", + "capabilities:b6f5727337fefe8e": "mdn_plus_5m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", + "capabilities:ed18cbc69ec23491": "mdn_plus_5m", + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqdupKb9q6OnNsL1TqMroUf" + }, + "mdn_plus_5y": { + "unit_amount": 5000, + "currency": "chf", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 5y - CHF IT", + "lookup_key": "chf-it-5y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "2", + "capabilities:b6f5727337fefe8e": "mdn_plus_5y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", + "capabilities:ed18cbc69ec23491": "mdn_plus_5y", + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqduqKb9q6OnNsLhNwOl77S" + }, + "mdn_plus_10m": { + "unit_amount": 1000, + "currency": "chf", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "month" + }, + "nickname": "MDN Plus 10m - CHF IT", + "lookup_key": "chf-it-10m", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "3", + "capabilities:b6f5727337fefe8e": "mdn_plus_10m", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", + "capabilities:ed18cbc69ec23491": "mdn_plus_10m", + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqduqKb9q6OnNsLZ9yMm4To" + }, + "mdn_plus_10y": { + "unit_amount": 10000, + "currency": "chf", + "product": "prod_Jtbg9tyGyLRuB0", + "recurring": { + "interval": "year" + }, + "nickname": "MDN Plus 10y - CHF IT", + "lookup_key": "chf-it-10y", + "metadata": { + "productSet": "mdnPlus", + "productOrder": "4", + "capabilities:b6f5727337fefe8e": "mdn_plus_10y", + "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", + "capabilities:ed18cbc69ec23491": "mdn_plus_10y", + "product:successActionButtonLabel": "Prosegui su MDN Plus", + "product:subtitle": "Un servizio Premium MDN", + "product:details:1": "Notifiche delle pagine", + "product:details:2": "Raccolte di articoli", + "product:details:3": "MDN offline" + }, + "price_id": "price_1KqdurKb9q6OnNsLUOz4fnk3" + } + } + } +} diff --git a/gcp/function/src/handlers/stripePlans.ts b/gcp/function/src/handlers/stripePlans.ts new file mode 100644 index 000000000000..ffe5027e04f7 --- /dev/null +++ b/gcp/function/src/handlers/stripePlans.ts @@ -0,0 +1,119 @@ +import type express from "express"; + +import { createRequire } from "node:module"; +import acceptLanguageParser from "accept-language-parser"; +import { ORIGIN_MAIN } from "../env.js"; + +const require = createRequire(import.meta.url); + +const stageLookup = require("./plans-stage-lookup.json") satisfies LookupData; +const prodLookup = require("./plans-prod-lookup.json") satisfies LookupData; + +type CountryCode = string; +type LanguageCode = string; +type CurrencyCode = string; +type ProductId = string; +type PriceId = string; + +interface Plan { + unit_amount: number; + currency: CurrencyCode; + product: ProductId; + recurring: { + interval: "month" | "year"; + }; + nickname: string; + lookup_key: string; + metadata: { [key: string]: string }; + price_id: PriceId; +} +interface LookupData { + countryToCurrency: { + [countryCode: CountryCode]: { + currency: CurrencyCode; + supportedLanguages: { + [languageCode: LanguageCode]: null | { + [key: string]: string; + }; + }; + defaultLanguage: LanguageCode; + }; + }; + langCurrencyToPlans: { + [langCurrencyCode: string]: { + [name: string]: Plan; + }; + }; +} + +interface PlanResult { + [name: string]: { monthlyPriceInCents: number; id: string }; +} +interface Result { + currency: CurrencyCode; + plans: PlanResult; +} + +export function stripePlans(req: express.Request, res: express.Response) { + const lookupData: LookupData = + ORIGIN_MAIN === "developer.mozilla.org" ? prodLookup : stageLookup; + + // https://cloud.google.com/appengine/docs/flexible/reference/request-headers#app_engine-specific_headers + const countryHeader = req.headers["x-appengine-country"]; + const localeHeader = req.headers["accept-language"]; + + const countryCode = + typeof countryHeader === "string" && countryHeader !== "ZZ" + ? countryHeader + : "US"; + + const supportedCurrency = lookupData.countryToCurrency[countryCode]; + + if (!supportedCurrency) { + return res.status(404); + } + + const acceptLanguage = typeof localeHeader === "string" ? localeHeader : null; + + let supportedLanguageOrDefault; + if (acceptLanguage) { + //E.g "en-GB,en;q=0.7,it;q=0.3" - Takes 'en' + supportedLanguageOrDefault = + acceptLanguageParser.pick( + Object.keys(supportedCurrency.supportedLanguages), + acceptLanguage, + { loose: true } + ) || supportedCurrency.defaultLanguage; + } else { + supportedLanguageOrDefault = supportedCurrency.defaultLanguage; + } + + const key = `${supportedCurrency.currency}-${supportedLanguageOrDefault}`; + + const plans = lookupData.langCurrencyToPlans[key]; + if (!plans) { + return res.status(500); + } + + const planResult: PlanResult = {}; + Object.entries(plans).forEach(([name, plan]) => { + let monthlyPriceInCents; + if (plan.recurring.interval === "year") { + monthlyPriceInCents = Math.floor(plan.unit_amount / 12); + } else { + monthlyPriceInCents = plan.unit_amount; + } + planResult[name] = { monthlyPriceInCents, id: plan.price_id }; + }); + + const result = { + currency: supportedCurrency.currency, + plans: planResult, + } satisfies Result; + + return res + .status(200) + .setHeader("Cache-Control", "max-age=86400") + .setHeader("Content-Type", "application/json") + .end(JSON.stringify(result)); +} From 286d3411ec0bbaed38140bb0bdf3b67cbb60b7d2 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 16 Mar 2023 17:14:23 +0100 Subject: [PATCH 071/343] chore(gcp/function): add country to stripePlan result --- gcp/function/src/handlers/stripePlans.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gcp/function/src/handlers/stripePlans.ts b/gcp/function/src/handlers/stripePlans.ts index ffe5027e04f7..91a5991b0930 100644 --- a/gcp/function/src/handlers/stripePlans.ts +++ b/gcp/function/src/handlers/stripePlans.ts @@ -50,6 +50,7 @@ interface PlanResult { [name: string]: { monthlyPriceInCents: number; id: string }; } interface Result { + country: CountryCode; currency: CurrencyCode; plans: PlanResult; } @@ -107,6 +108,7 @@ export function stripePlans(req: express.Request, res: express.Response) { }); const result = { + country: countryCode, currency: supportedCurrency.currency, plans: planResult, } satisfies Result; From 411df4afc96934bc8fb4c5b06dce5c26e59c7f3e Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 16 Mar 2023 17:24:43 +0100 Subject: [PATCH 072/343] chore(gcp/function): use bcd.developer.mozilla.org --- gcp/function/src/env.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/function/src/env.ts b/gcp/function/src/env.ts index 8170017a7ac3..7e44f9aaf70a 100644 --- a/gcp/function/src/env.ts +++ b/gcp/function/src/env.ts @@ -72,7 +72,7 @@ export const SOURCE_LIVE_SAMPLES: string = resolveSource( "https://yari-demos.prod.mdn.mozit.cloud" ); export const SOURCE_BCD_API: string = - process.env["SOURCE_BCD_API"] || "https://developer.mozilla.org"; + process.env["SOURCE_BCD_API"] || "https://bcd.developer.mozilla.org"; export const SOURCE_CLIENT: string = resolveSource( process.env["SOURCE_CLIENT"] || "https://developer.mozilla.org" ); From f61ee017464c01dabcbe93422063b9542779679b Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 16 Mar 2023 17:36:50 +0100 Subject: [PATCH 073/343] feat(gcp/function): forward telemetry --- gcp/function/src/app.ts | 2 ++ gcp/function/src/handlers/telemetry.ts | 12 ++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 gcp/function/src/handlers/telemetry.ts diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 3e6d2de47f6f..9328c707f437 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -6,6 +6,7 @@ import { client } from "./handlers/client.js"; import { bcdApi } from "./handlers/bcdApi.js"; import { rumba } from "./handlers/rumba.js"; import { stripePlans } from "./handlers/stripePlans.js"; +import { telemetry } from "./handlers/telemetry.js"; import { pathnameLC } from "./middlewares/pathnameLC.js"; import { contentOriginRequest } from "./middlewares/content-origin-request.js"; @@ -15,6 +16,7 @@ mainRouter.get("/bcd/api/*", bcdApi()); mainRouter.all("/api/v1/stripe/plans", stripePlans); mainRouter.all("/api/*", rumba); mainRouter.all("/users/fxa/*", rumba); +mainRouter.all("/submit/mdn-yari/*", telemetry); mainRouter.use(contentOriginRequest); mainRouter.get("/[^/]+/docs/*", docsHandler); mainRouter.get("/[^/]+/search-index.json", docsHandler); diff --git a/gcp/function/src/handlers/telemetry.ts b/gcp/function/src/handlers/telemetry.ts new file mode 100644 index 000000000000..56a22239a1fe --- /dev/null +++ b/gcp/function/src/handlers/telemetry.ts @@ -0,0 +1,12 @@ +import httpProxy from "http-proxy"; +import type express from "express"; + +const telemetryProxy = httpProxy.createProxy({ + target: "https://incoming.telemetry.mozilla.org", + changeOrigin: true, + autoRewrite: true, +}); + +export function telemetry(req: express.Request, res: express.Response) { + telemetryProxy.web(req, res); +} From 657babade34c34a85f2d6bf0fde411e301014503 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 16 Mar 2023 17:37:04 +0100 Subject: [PATCH 074/343] ci(xyz-build): enable glean --- .github/workflows/xyz-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 792129225605..e637e8c90549 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -115,8 +115,8 @@ jobs: REACT_APP_SURVEY_RATE_TILL_CONTENT_DISCOVERY_2023: 0.05 # 5% # Telemetry. - REACT_APP_GLEAN_CHANNEL: stage - REACT_APP_GLEAN_ENABLED: false + REACT_APP_GLEAN_CHANNEL: xyz + REACT_APP_GLEAN_ENABLED: true # Newsletter REACT_APP_NEWSLETTER_ENABLED: false From 22b39cc40acb4ab6a75bb7fbcd83452bb3e181a1 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 16 Mar 2023 18:10:37 +0100 Subject: [PATCH 075/343] refactor(gcp/function): extract getRequestCountry() --- gcp/function/src/handlers/stripePlans.ts | 8 ++------ gcp/function/src/utils.ts | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/gcp/function/src/handlers/stripePlans.ts b/gcp/function/src/handlers/stripePlans.ts index 91a5991b0930..1a673f629c07 100644 --- a/gcp/function/src/handlers/stripePlans.ts +++ b/gcp/function/src/handlers/stripePlans.ts @@ -3,6 +3,7 @@ import type express from "express"; import { createRequire } from "node:module"; import acceptLanguageParser from "accept-language-parser"; import { ORIGIN_MAIN } from "../env.js"; +import { getRequestCountry } from "../utils.js"; const require = createRequire(import.meta.url); @@ -59,14 +60,9 @@ export function stripePlans(req: express.Request, res: express.Response) { const lookupData: LookupData = ORIGIN_MAIN === "developer.mozilla.org" ? prodLookup : stageLookup; - // https://cloud.google.com/appengine/docs/flexible/reference/request-headers#app_engine-specific_headers - const countryHeader = req.headers["x-appengine-country"]; const localeHeader = req.headers["accept-language"]; - const countryCode = - typeof countryHeader === "string" && countryHeader !== "ZZ" - ? countryHeader - : "US"; + const countryCode = getRequestCountry(req); const supportedCurrency = lookupData.countryToCurrency[countryCode]; diff --git a/gcp/function/src/utils.ts b/gcp/function/src/utils.ts index 5a18596d6a61..1c1c8b87e6f7 100644 --- a/gcp/function/src/utils.ts +++ b/gcp/function/src/utils.ts @@ -1,5 +1,6 @@ -import { slugToFolder } from "@yari-internal/slug-utils"; import * as path from "node:path"; +import type express from "express"; +import { slugToFolder } from "@yari-internal/slug-utils"; export function resolveIndexHTML(pathOrUrl: string) { let resolvedPath = slugToFolder(pathOrUrl); @@ -8,3 +9,16 @@ export function resolveIndexHTML(pathOrUrl: string) { } return resolvedPath; } + +const DEFAULT_COUNTRY = "US"; + +export function getRequestCountry(req: express.Request): string { + // https://cloud.google.com/appengine/docs/flexible/reference/request-headers#app_engine-specific_headers + const value = req.headers["x-appengine-country"]; + + if (typeof value === "string" && value !== "ZZ") { + return value; + } else { + return DEFAULT_COUNTRY; + } +} From 76476a897cd43d78baa22beb6db66921da27f75f Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 16 Mar 2023 19:07:38 +0100 Subject: [PATCH 076/343] feat(gcp/function): add kevel handler --- gcp/function/package-lock.json | 121 +++++++++++++++++ gcp/function/package.json | 1 + gcp/function/src/app.ts | 3 + gcp/function/src/env.ts | 9 ++ gcp/function/src/handlers/cc2ip.ts | 173 ++++++++++++++++++++++++ gcp/function/src/handlers/kevel.ts | 210 +++++++++++++++++++++++++++++ 6 files changed, 517 insertions(+) create mode 100644 gcp/function/src/handlers/cc2ip.ts create mode 100644 gcp/function/src/handlers/kevel.ts diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index 831cfa2cf658..cf353b2b05d7 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "MPL-2.0", "dependencies": { + "@adzerk/decision-sdk": "^1.0.0-beta.20", "@google-cloud/functions-framework": "^3.1.3", "@yari-internal/constants": "file:src/internal/constants", "@yari-internal/fundamental-redirects": "file:src/internal/fundamental-redirects", @@ -83,6 +84,37 @@ "../libs/slug-utils": { "extraneous": true }, + "node_modules/@adzerk/decision-sdk": { + "version": "1.0.0-beta.20", + "resolved": "https://registry.npmjs.org/@adzerk/decision-sdk/-/decision-sdk-1.0.0-beta.20.tgz", + "integrity": "sha512-IgEqZoELahKP0Q8oGYXpv50qf69D9PXeYWO7ImK+RLemYI0YsskE8Jr2ixRieXw0Dt77k4vSZz7FGMIChW0TsQ==", + "dependencies": { + "debug": "^4.1.1", + "form-data": "^2.5.1", + "isomorphic-unfetch": "^3.1.0" + } + }, + "node_modules/@adzerk/decision-sdk/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@adzerk/decision-sdk/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/@babel/code-frame": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", @@ -578,6 +610,11 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -689,6 +726,17 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -782,6 +830,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1043,6 +1099,19 @@ "is-callable": "^1.1.3" } }, + "node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1534,6 +1603,15 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isomorphic-unfetch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz", + "integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==", + "dependencies": { + "node-fetch": "^2.6.1", + "unfetch": "^4.2.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1715,6 +1793,25 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node_modules/node-fetch": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", + "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -2355,6 +2452,11 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -2468,6 +2570,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unfetch": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", + "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -2540,6 +2647,20 @@ "node": ">= 0.8" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/gcp/function/package.json b/gcp/function/package.json index f1fdc19f9816..6e0e22960c64 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -18,6 +18,7 @@ "start": "ts-node src/cli.ts" }, "dependencies": { + "@adzerk/decision-sdk": "^1.0.0-beta.20", "@google-cloud/functions-framework": "^3.1.3", "@yari-internal/constants": "file:src/internal/constants", "@yari-internal/fundamental-redirects": "file:src/internal/fundamental-redirects", diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 9328c707f437..b59741077f52 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -4,6 +4,7 @@ import { Origin, origin } from "./env.js"; import { docs } from "./handlers/content.js"; import { client } from "./handlers/client.js"; import { bcdApi } from "./handlers/bcdApi.js"; +import { kevel } from "./handlers/kevel.js"; import { rumba } from "./handlers/rumba.js"; import { stripePlans } from "./handlers/stripePlans.js"; import { telemetry } from "./handlers/telemetry.js"; @@ -17,6 +18,8 @@ mainRouter.all("/api/v1/stripe/plans", stripePlans); mainRouter.all("/api/*", rumba); mainRouter.all("/users/fxa/*", rumba); mainRouter.all("/submit/mdn-yari/*", telemetry); +mainRouter.all("/pong/*", kevel); +mainRouter.all("/pimg/*", kevel); mainRouter.use(contentOriginRequest); mainRouter.get("/[^/]+/docs/*", docsHandler); mainRouter.get("/[^/]+/search-index.json", docsHandler); diff --git a/gcp/function/src/env.ts b/gcp/function/src/env.ts index 7e44f9aaf70a..541552efd75f 100644 --- a/gcp/function/src/env.ts +++ b/gcp/function/src/env.ts @@ -101,3 +101,12 @@ export function sourceUri(source: Source): string { return ""; } } + +// Kevel. +export const KEVEL_SITE_ID = Number(process.env["KEVEL_SITE_ID"] ?? 0); +export const KEVEL_NETWORK_ID = Number(process.env["KEVEL_NETWORK_ID"] ?? 0); +export const SIGN_SECRET = process.env["SIGN_SECRET"] ?? ""; +export const CARBON_ZONE_KEY = process.env["CARBON_ZONE_KEY"] ?? ""; +export const CARBON_FALLBACK_ENABLED = Boolean( + JSON.parse(process.env["CARBON_FALLBACK_ENABLED"] || "false") +); diff --git a/gcp/function/src/handlers/cc2ip.ts b/gcp/function/src/handlers/cc2ip.ts new file mode 100644 index 000000000000..e6b002a1cf19 --- /dev/null +++ b/gcp/function/src/handlers/cc2ip.ts @@ -0,0 +1,173 @@ +export const CC_TO_IP: { [countryCode: string]: string } = { + AD: "46.172.232.102", + AE: "94.200.156.82", + AF: "103.146.146.97", + AG: "66.249.150.51", + AL: "192.109.219.158", + AM: "37.186.119.247", + AO: "41.63.188.109", + AQ: "2a05:dfc7:5353::53", + AR: "45.65.225.220", + AS: "202.70.125.9", + AT: "195.234.100.25", + AU: "203.147.94.71", + AZ: "85.132.17.38", + BA: "217.23.199.77", + BB: "64.210.41.182", + BD: "103.245.96.174", + BE: "194.6.227.60", + BF: "41.216.155.125", + BG: "194.12.224.14", + BH: "157.175.58.241", + BJ: "154.66.142.8", + BM: "199.68.195.249", + BN: "61.6.225.234", + BO: "190.186.245.140", + BQ: "190.107.252.178", + BR: "131.72.141.149", + BW: "154.73.39.241", + BY: "195.222.86.106", + BZ: "200.123.208.126", + CA: "23.252.239.1", + CD: "102.68.154.33", + CG: "41.207.125.57", + CH: "34.65.77.163", + CL: "45.7.228.232", + CM: "41.211.108.4", + CN: "223.6.6.195", + CO: "190.156.237.85", + CR: "190.106.79.228", + CU: "152.206.201.137", + CY: "46.199.74.102", + CZ: "188.116.91.246", + DE: "188.68.45.12", + DJ: "196.201.198.165", + DK: "152.115.91.203", + DO: "181.232.190.197", + DZ: "41.100.61.49", + EC: "181.198.11.152", + EE: "188.127.234.193", + EG: "41.176.151.71", + ES: "185.2.68.79", + ET: "196.188.168.146", + FI: "87.92.251.93", + FJ: "202.129.231.250", + FR: "217.111.148.201", + GA: "41.158.1.162", + GB: "87.242.157.62", + GE: "188.169.44.18", + GF: "217.108.102.6", + GH: "102.176.81.182", + GM: "197.148.74.19", + GN: "102.176.160.107", + GP: "81.248.145.154", + GQ: "102.164.255.149", + GR: "2.84.38.146", + GT: "38.52.208.199", + GU: "114.142.243.170", + HK: "202.155.202.75", + HN: "190.185.118.104", + HR: "85.114.40.50", + HU: "79.172.213.213", + ID: "175.103.40.42", + IE: "86.43.125.181", + IL: "31.154.9.62", + IM: "185.246.128.162", + IN: "164.52.223.153", + IQ: "213.32.252.91", + IR: "2.188.21.131", + IS: "193.4.89.2", + IT: "217.70.144.8", + JM: "63.143.125.14", + JO: "92.253.127.65", + JP: "153.182.23.230", + KE: "197.232.155.222", + KG: "92.62.65.237", + KH: "116.212.143.233", + KR: "121.124.124.196", + KW: "195.39.131.78", + KY: "216.144.84.145", + KZ: "87.76.54.213", + LA: "202.137.128.6", + LB: "89.108.139.246", + LK: "203.143.42.24", + LT: "88.119.87.88", + LU: "185.44.142.137", + LV: "86.63.169.194", + LY: "165.16.39.113", + MA: "197.230.103.202", + MD: "212.28.88.241", + ME: "95.155.31.120", + MH: "103.202.148.251", + MK: "146.255.89.51", + MM: "103.115.23.44", + MN: "202.55.190.30", + MO: "182.93.25.100", + MR: "82.151.74.36", + MT: "194.105.32.2", + MU: "41.216.125.179", + MV: "202.1.194.16", + MW: "41.216.228.228", + MX: "189.234.24.31", + MY: "60.51.247.44", + MZ: "197.219.229.86", + NA: "197.234.98.210", + NC: "202.22.148.124", + NG: "41.184.148.252", + NI: "200.62.96.39", + NL: "185.81.8.252", + NO: "80.202.239.184", + NP: "103.126.245.139", + NZ: "121.79.252.57", + OM: "5.21.239.143", + PA: "190.140.202.124", + PE: "200.37.203.90", + PF: "113.197.68.20", + PG: "124.240.199.23", + PH: "124.107.101.26", + PK: "110.39.8.113", + PL: "80.54.219.186", + PR: "12.205.65.7", + PS: "213.6.32.10", + PT: "62.28.205.202", + PW: "202.124.226.133", + PY: "201.217.51.46", + RE: "102.35.162.43", + RO: "188.27.244.73", + RS: "185.248.172.8", + RU: "89.110.59.24", + RW: "41.215.248.143", + SA: "51.211.38.5", + SB: "202.1.172.187", + SC: "185.247.225.17", + SD: "196.1.210.35", + SE: "158.174.37.226", + SG: "119.75.28.242", + SI: "195.230.121.12", + SK: "87.197.154.105", + SN: "213.154.80.203", + SV: "190.87.164.207", + SY: "5.134.255.230", + SZ: "102.215.99.18", + TD: "102.223.194.134", + TG: "41.207.186.166", + TH: "1.4.206.84", + TJ: "85.9.129.36", + TN: "197.15.87.14", + TR: "176.236.129.141", + TT: "200.1.104.36", + TW: "36.237.20.227", + TZ: "41.59.200.123", + UA: "176.126.123.42", + UG: "154.72.199.202", + US: "68.228.28.247", + UY: "190.64.151.74", + UZ: "185.183.242.130", + VE: "190.75.2.41", + VI: "8.26.19.118", + VN: "171.235.173.79", + YE: "134.35.132.212", + YT: "41.242.116.25", + ZA: "102.36.123.181", + ZW: "41.174.104.223", +}; diff --git a/gcp/function/src/handlers/kevel.ts b/gcp/function/src/handlers/kevel.ts new file mode 100644 index 000000000000..6fbf2f7e6ac7 --- /dev/null +++ b/gcp/function/src/handlers/kevel.ts @@ -0,0 +1,210 @@ +/* global fetch */ +import { createHmac } from "node:crypto"; +import * as url from "node:url"; + +import type express from "express"; +import { Client } from "@adzerk/decision-sdk"; +import { + KEVEL_SITE_ID, + KEVEL_NETWORK_ID, + SIGN_SECRET, + CARBON_ZONE_KEY, + CARBON_FALLBACK_ENABLED, +} from "../env.js"; +import { CC_TO_IP } from "./cc2ip.js"; +import { getRequestCountry } from "../utils.js"; + +const siteId = KEVEL_SITE_ID; +const networkId = KEVEL_NETWORK_ID; +const client = new Client({ networkId, siteId }); + +export async function fetchImage(src: string) { + const imageResponse = await fetch(src); + const imageBuffer = await imageResponse.arrayBuffer(); + const contentType = imageResponse.headers.get("content-type"); + return { buf: imageBuffer, contentType }; +} + +function encodeAndSign(s: string): string { + const hmac = createHmac("sha256", SIGN_SECRET); + hmac.update(s); + return `${Buffer.from(s, "utf-8").toString("base64")}.${hmac.digest( + "base64" + )}`; +} + +function decodeAndVerify(tuple: string): string | null { + if (tuple === null) { + return null; + } + const [encoded, digest] = tuple.split("."); + if (!encoded || !digest) { + return null; + } + const s = Buffer.from(encoded, "base64").toString("utf-8"); + const hmac = createHmac("sha256", SIGN_SECRET); + hmac.update(s); + if (hmac.digest("base64") == digest) { + // === won't work... + return s; + } + return null; +} + +export async function kevel(req: express.Request, res: express.Response) { + const countryCode = getRequestCountry(req); + const anonymousIp = CC_TO_IP[countryCode] ?? "127.0.0.1"; + + const userAgent = req.headers["user-agent"] ?? null; + + const parsedUrl = url.parse(req.url); + if (parsedUrl.pathname === "/pong/get") { + if (req.method !== "POST") { + return res.status(405).end(); + } + + const { keywords = [] } = JSON.parse( + Buffer.from(req.body.data, "base64").toString() + ); + const decisionReq = { + placements: [{ adTypes: [465, 369] }], + keywords: [...keywords, countryCode], + }; + + const decisionRes = await client.decisions.get(decisionReq, { + ip: anonymousIp, + } as any); + const { decisions: { div0 } = {} } = decisionRes; + if (div0 === null || div0?.[0] === null) { + return res.status(204).end(); + } + + let payload = {}; + + const [{ contents, clickUrl, impressionUrl }] = div0 as any; + if ( + CARBON_FALLBACK_ENABLED && + CARBON_ZONE_KEY && + CARBON_ZONE_KEY !== "undefined" && + contents?.[0]?.data?.customData?.fallback + ) { + // fall back to carbon + try { + const { + ads: [ + { description = null, statlink, statimp, smallImage, ad_via_link }, + ] = [], + } = await ( + await fetch( + `https://srv.buysellads.com/ads/${CARBON_ZONE_KEY}.json?forwardedip=${encodeURIComponent( + anonymousIp + )}${userAgent ? `&useragent=${encodeURIComponent(userAgent)}` : ""}` + ) + ).json(); + payload = { + click: encodeAndSign(clickUrl), + view: encodeAndSign(impressionUrl), + fallback: { + click: encodeAndSign(statlink), + view: encodeAndSign(statimp), + image: encodeAndSign(smallImage), + copy: description, + by: ad_via_link, + }, + }; + } catch (e) { + console.log(e); + return res.status(400).end(); + } + } else { + payload = { + copy: contents?.[0]?.data?.title || "This is an ad without copy?!", + image: encodeAndSign(contents[0]?.data?.imageUrl), + click: encodeAndSign(clickUrl), + view: encodeAndSign(impressionUrl), + }; + } + + return res + .status(200) + .setHeader("cache-control", "no-store") + .setHeader("content-type", "application/json") + .end(JSON.stringify(payload)); + } else if (req.path === "/pong/click") { + if (req.method !== "GET") { + return res.status(405).end(); + } + const params = new URLSearchParams(parsedUrl.search ?? ""); + try { + const click = decodeAndVerify(params.get("code") ?? ""); + const fallback = decodeAndVerify(params.get("fallback") ?? ""); + + if (!click) { + return res.status(400).end(); + } + + const fetchRes = await fetch(click, { redirect: "manual" }); + let status = fetchRes.status; + let headers = fetchRes.headers; + if (fallback) { + const fallbackRes = await fetch(`https:${fallback}`, { + redirect: "manual", + }); + status = fallbackRes.status; + headers = fallbackRes.headers; + } + const location = headers.get("location"); + if (location && (status === 301 || status === 302)) { + return res.redirect(location); + } else { + return res.status(502).end(); + } + } catch (e) { + console.error(e); + return res.status(500).end(); + } + } else if (parsedUrl.pathname === "/pong/viewed") { + if (req.method !== "POST") { + return { + status: 405, + statusDescription: "METHOD_NOT_ALLOWED", + }; + } + const params = new URLSearchParams(parsedUrl.search ?? undefined); + try { + const view = decodeAndVerify(params.get("code") ?? ""); + const fallback = decodeAndVerify(params.get("fallback") ?? ""); + fallback && (await fetch(`https:${fallback}`, { redirect: "manual" })); + + if (!view) { + return res.status(400).end(); + } + + await fetch(view, { redirect: "manual" }); + return res.status(201).end(); + } catch (e) { + console.error(e); + return res.status(500).end(); + } + } else if (req.url.startsWith("/pimg/")) { + const src = decodeAndVerify( + decodeURIComponent(req.url.substring("/pimg/".length)) + ); + + if (!src) { + return res.status(400).end(); + } + + const { buf, contentType } = await fetchImage(src); + return res + .status(200) + .set({ + "cache-control": "max-age=86400", + "content-type": contentType, + "content-transfer-encoding": "base64", + }) + .end(Buffer.from(buf).toString("base64")); + } + + return res.status(204).end(); +} From 85e8eec20685f3c980ee55663d7da8bd33ce0116 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 16 Mar 2023 19:09:27 +0100 Subject: [PATCH 077/343] ci(xyz-build): enable placements --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index e637e8c90549..ff83440315f7 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -122,7 +122,7 @@ jobs: REACT_APP_NEWSLETTER_ENABLED: false # Placement - REACT_APP_PLACEMENT_ENABLED: false + REACT_APP_PLACEMENT_ENABLED: true run: | From 2b37af663d6851ca3a123cf5aa549eecc494f615 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 16 Mar 2023 20:20:08 +0100 Subject: [PATCH 078/343] fixup! feat(gcp/function): add kevel handler --- gcp/function/src/app.ts | 4 ++-- gcp/function/src/handlers/kevel.ts | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index b59741077f52..51b99cd45200 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -1,4 +1,4 @@ -import type express from "express"; +import express from "express"; import { Router } from "express"; import { Origin, origin } from "./env.js"; import { docs } from "./handlers/content.js"; @@ -18,7 +18,7 @@ mainRouter.all("/api/v1/stripe/plans", stripePlans); mainRouter.all("/api/*", rumba); mainRouter.all("/users/fxa/*", rumba); mainRouter.all("/submit/mdn-yari/*", telemetry); -mainRouter.all("/pong/*", kevel); +mainRouter.all("/pong/*", express.json(), kevel); mainRouter.all("/pimg/*", kevel); mainRouter.use(contentOriginRequest); mainRouter.get("/[^/]+/docs/*", docsHandler); diff --git a/gcp/function/src/handlers/kevel.ts b/gcp/function/src/handlers/kevel.ts index 6fbf2f7e6ac7..1261c6479f26 100644 --- a/gcp/function/src/handlers/kevel.ts +++ b/gcp/function/src/handlers/kevel.ts @@ -58,14 +58,15 @@ export async function kevel(req: express.Request, res: express.Response) { const userAgent = req.headers["user-agent"] ?? null; const parsedUrl = url.parse(req.url); - if (parsedUrl.pathname === "/pong/get") { + const pathname = parsedUrl.pathname ?? ""; + const search = parsedUrl.search ?? ""; + + if (pathname === "/pong/get") { if (req.method !== "POST") { return res.status(405).end(); } - const { keywords = [] } = JSON.parse( - Buffer.from(req.body.data, "base64").toString() - ); + const { keywords = [] } = req.body; const decisionReq = { placements: [{ adTypes: [465, 369] }], keywords: [...keywords, countryCode], @@ -134,7 +135,7 @@ export async function kevel(req: express.Request, res: express.Response) { if (req.method !== "GET") { return res.status(405).end(); } - const params = new URLSearchParams(parsedUrl.search ?? ""); + const params = new URLSearchParams(search); try { const click = decodeAndVerify(params.get("code") ?? ""); const fallback = decodeAndVerify(params.get("fallback") ?? ""); @@ -163,14 +164,14 @@ export async function kevel(req: express.Request, res: express.Response) { console.error(e); return res.status(500).end(); } - } else if (parsedUrl.pathname === "/pong/viewed") { + } else if (pathname === "/pong/viewed") { if (req.method !== "POST") { return { status: 405, statusDescription: "METHOD_NOT_ALLOWED", }; } - const params = new URLSearchParams(parsedUrl.search ?? undefined); + const params = new URLSearchParams(search); try { const view = decodeAndVerify(params.get("code") ?? ""); const fallback = decodeAndVerify(params.get("fallback") ?? ""); @@ -186,9 +187,9 @@ export async function kevel(req: express.Request, res: express.Response) { console.error(e); return res.status(500).end(); } - } else if (req.url.startsWith("/pimg/")) { + } else if (pathname.startsWith("/pimg/")) { const src = decodeAndVerify( - decodeURIComponent(req.url.substring("/pimg/".length)) + decodeURIComponent(pathname.substring("/pimg/".length)) ); if (!src) { @@ -201,9 +202,8 @@ export async function kevel(req: express.Request, res: express.Response) { .set({ "cache-control": "max-age=86400", "content-type": contentType, - "content-transfer-encoding": "base64", }) - .end(Buffer.from(buf).toString("base64")); + .end(Buffer.from(buf)); } return res.status(204).end(); From 81abf2ab8d9762b0abebb3a5b3055ec91bce1762 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 16 Mar 2023 20:43:16 +0100 Subject: [PATCH 079/343] feat(gcp/app): forward /admin-api + /events/fxa to rumba --- gcp/function/src/app.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 51b99cd45200..a5b33999071d 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -16,6 +16,8 @@ const docsHandler = docs(); mainRouter.get("/bcd/api/*", bcdApi()); mainRouter.all("/api/v1/stripe/plans", stripePlans); mainRouter.all("/api/*", rumba); +mainRouter.all("/admin-api/*", rumba); +mainRouter.all("/events/fxa/*", rumba); mainRouter.all("/users/fxa/*", rumba); mainRouter.all("/submit/mdn-yari/*", telemetry); mainRouter.all("/pong/*", express.json(), kevel); From 615eb0a3091a1b70b31b4338c0be0ee6fdd10825 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 20 Mar 2023 11:24:12 +0100 Subject: [PATCH 080/343] fix(gcp/function): use contentOriginRequest only for specific routes --- gcp/function/src/app.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index a5b33999071d..2e51b30d91f6 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -22,10 +22,9 @@ mainRouter.all("/users/fxa/*", rumba); mainRouter.all("/submit/mdn-yari/*", telemetry); mainRouter.all("/pong/*", express.json(), kevel); mainRouter.all("/pimg/*", kevel); -mainRouter.use(contentOriginRequest); -mainRouter.get("/[^/]+/docs/*", docsHandler); -mainRouter.get("/[^/]+/search-index.json", docsHandler); -mainRouter.get("*", client()); +mainRouter.get("/[^/]+/docs/*", contentOriginRequest, docsHandler); +mainRouter.get("/[^/]+/search-index.json", contentOriginRequest, docsHandler); +mainRouter.get("*", contentOriginRequest, client()); const liveSampleRouter = Router(); liveSampleRouter.use(pathnameLC); From b812ad1b480c98b72d8791413e03a3f01139d854 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 20 Mar 2023 17:21:06 +0100 Subject: [PATCH 081/343] refactor(gcp/function): rename handlers --- gcp/function/src/app.ts | 43 ++++++++++++++------------ gcp/function/src/handlers/bcdApi.ts | 2 +- gcp/function/src/handlers/client.ts | 2 +- gcp/function/src/handlers/content.ts | 2 +- gcp/function/src/handlers/kevel.ts | 2 +- gcp/function/src/handlers/rumba.ts | 2 +- gcp/function/src/handlers/telemetry.ts | 2 +- 7 files changed, 29 insertions(+), 26 deletions(-) diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 2e51b30d91f6..05f5c0fbd572 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -1,35 +1,38 @@ import express from "express"; import { Router } from "express"; import { Origin, origin } from "./env.js"; -import { docs } from "./handlers/content.js"; -import { client } from "./handlers/client.js"; -import { bcdApi } from "./handlers/bcdApi.js"; -import { kevel } from "./handlers/kevel.js"; -import { rumba } from "./handlers/rumba.js"; +import { createContentProxy } from "./handlers/content.js"; +import { proxyClient } from "./handlers/client.js"; +import { proxyBcdApi } from "./handlers/bcdApi.js"; +import { proxyKevel } from "./handlers/kevel.js"; +import { proxyRumba } from "./handlers/rumba.js"; import { stripePlans } from "./handlers/stripePlans.js"; -import { telemetry } from "./handlers/telemetry.js"; +import { proxyTelemetry } from "./handlers/telemetry.js"; import { pathnameLC } from "./middlewares/pathnameLC.js"; import { contentOriginRequest } from "./middlewares/content-origin-request.js"; const mainRouter = Router(); -const docsHandler = docs(); -mainRouter.get("/bcd/api/*", bcdApi()); +const proxyContent = createContentProxy(); +mainRouter.get("/bcd/api/*", proxyBcdApi()); mainRouter.all("/api/v1/stripe/plans", stripePlans); -mainRouter.all("/api/*", rumba); -mainRouter.all("/admin-api/*", rumba); -mainRouter.all("/events/fxa/*", rumba); -mainRouter.all("/users/fxa/*", rumba); -mainRouter.all("/submit/mdn-yari/*", telemetry); -mainRouter.all("/pong/*", express.json(), kevel); -mainRouter.all("/pimg/*", kevel); -mainRouter.get("/[^/]+/docs/*", contentOriginRequest, docsHandler); -mainRouter.get("/[^/]+/search-index.json", contentOriginRequest, docsHandler); -mainRouter.get("*", contentOriginRequest, client()); +mainRouter.all("/api/*", proxyRumba); +mainRouter.all("/admin-api/*", proxyRumba); +mainRouter.all("/events/fxa/*", proxyRumba); +mainRouter.all("/users/fxa/*", proxyRumba); +mainRouter.all("/submit/mdn-yari/*", proxyTelemetry); +mainRouter.all("/pong/*", express.json(), proxyKevel); +mainRouter.all("/pimg/*", proxyKevel); +mainRouter.get("/[^/]+/docs/*", contentOriginRequest, proxyContent); +mainRouter.get("/[^/]+/search-index.json", contentOriginRequest, proxyContent); +mainRouter.get("*", contentOriginRequest, proxyClient()); const liveSampleRouter = Router(); liveSampleRouter.use(pathnameLC); -liveSampleRouter.get("/[^/]+/docs/*/_sample_.*.html", client()); -liveSampleRouter.get("/[^/]+/docs/*/*.(png|jpeg|jpg|gif|svg|webp)", client()); +liveSampleRouter.get("/[^/]+/docs/*/_sample_.*.html", proxyClient()); +liveSampleRouter.get( + "/[^/]+/docs/*/*.(png|jpeg|jpg|gif|svg|webp)", + proxyClient() +); liveSampleRouter.get("*", (_req: express.Request, res: express.Response) => res.status(404).send() ); diff --git a/gcp/function/src/handlers/bcdApi.ts b/gcp/function/src/handlers/bcdApi.ts index b16c680f1d2d..6b779e3732e6 100644 --- a/gcp/function/src/handlers/bcdApi.ts +++ b/gcp/function/src/handlers/bcdApi.ts @@ -5,7 +5,7 @@ import { Source } from "../env.js"; import { responder } from "../source.js"; import { withResponseHeaders, withProxyResponseHeaders } from "../headers.js"; -export function bcdApi(): express.Handler { +export function proxyBcdApi(): express.Handler { return responder({ source: Source.bcdApi, http(source) { diff --git a/gcp/function/src/handlers/client.ts b/gcp/function/src/handlers/client.ts index 9200af06cfe3..bd372efd5959 100644 --- a/gcp/function/src/handlers/client.ts +++ b/gcp/function/src/handlers/client.ts @@ -6,7 +6,7 @@ import { responder } from "../source.js"; import { resolveIndexHTML } from "../utils.js"; import { withResponseHeaders, withProxyResponseHeaders } from "../headers.js"; -export function client(): express.Handler { +export function proxyClient(): express.Handler { return responder({ source: Source.client, http(source) { diff --git a/gcp/function/src/handlers/content.ts b/gcp/function/src/handlers/content.ts index 11c592d41a97..5def19b8a7e7 100644 --- a/gcp/function/src/handlers/content.ts +++ b/gcp/function/src/handlers/content.ts @@ -6,7 +6,7 @@ import { Source } from "../env.js"; import httpProxy from "http-proxy"; import { resolveIndexHTML } from "../utils.js"; -export function docs(): express.Handler { +export function createContentProxy(): express.Handler { return responder({ source: Source.content, http(source) { diff --git a/gcp/function/src/handlers/kevel.ts b/gcp/function/src/handlers/kevel.ts index 1261c6479f26..f05fff27ad02 100644 --- a/gcp/function/src/handlers/kevel.ts +++ b/gcp/function/src/handlers/kevel.ts @@ -51,7 +51,7 @@ function decodeAndVerify(tuple: string): string | null { return null; } -export async function kevel(req: express.Request, res: express.Response) { +export async function proxyKevel(req: express.Request, res: express.Response) { const countryCode = getRequestCountry(req); const anonymousIp = CC_TO_IP[countryCode] ?? "127.0.0.1"; diff --git a/gcp/function/src/handlers/rumba.ts b/gcp/function/src/handlers/rumba.ts index 981f5f70e112..10e0e98e7530 100644 --- a/gcp/function/src/handlers/rumba.ts +++ b/gcp/function/src/handlers/rumba.ts @@ -8,6 +8,6 @@ const rumbaProxy = httpProxy.createProxy({ autoRewrite: true, }); -export function rumba(req: express.Request, res: express.Response) { +export function proxyRumba(req: express.Request, res: express.Response) { rumbaProxy.web(req, res); } diff --git a/gcp/function/src/handlers/telemetry.ts b/gcp/function/src/handlers/telemetry.ts index 56a22239a1fe..2e0c38350af0 100644 --- a/gcp/function/src/handlers/telemetry.ts +++ b/gcp/function/src/handlers/telemetry.ts @@ -7,6 +7,6 @@ const telemetryProxy = httpProxy.createProxy({ autoRewrite: true, }); -export function telemetry(req: express.Request, res: express.Response) { +export function proxyTelemetry(req: express.Request, res: express.Response) { telemetryProxy.web(req, res); } From 2f03dc382f62cf4e470685fb1159f1425d99fc7b Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 21 Mar 2023 17:13:49 +0100 Subject: [PATCH 082/343] chore(gcp/function): merge {client,content} handler --- gcp/function/src/app.ts | 7 +++-- gcp/function/src/env.ts | 6 ----- gcp/function/src/handlers/client.ts | 38 ---------------------------- gcp/function/src/handlers/content.ts | 13 +++++----- 4 files changed, 10 insertions(+), 54 deletions(-) delete mode 100644 gcp/function/src/handlers/client.ts diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 05f5c0fbd572..011712804df9 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -2,7 +2,6 @@ import express from "express"; import { Router } from "express"; import { Origin, origin } from "./env.js"; import { createContentProxy } from "./handlers/content.js"; -import { proxyClient } from "./handlers/client.js"; import { proxyBcdApi } from "./handlers/bcdApi.js"; import { proxyKevel } from "./handlers/kevel.js"; import { proxyRumba } from "./handlers/rumba.js"; @@ -24,14 +23,14 @@ mainRouter.all("/pong/*", express.json(), proxyKevel); mainRouter.all("/pimg/*", proxyKevel); mainRouter.get("/[^/]+/docs/*", contentOriginRequest, proxyContent); mainRouter.get("/[^/]+/search-index.json", contentOriginRequest, proxyContent); -mainRouter.get("*", contentOriginRequest, proxyClient()); +mainRouter.get("*", contentOriginRequest, proxyContent); const liveSampleRouter = Router(); liveSampleRouter.use(pathnameLC); -liveSampleRouter.get("/[^/]+/docs/*/_sample_.*.html", proxyClient()); +liveSampleRouter.get("/[^/]+/docs/*/_sample_.*.html", proxyContent); liveSampleRouter.get( "/[^/]+/docs/*/*.(png|jpeg|jpg|gif|svg|webp)", - proxyClient() + proxyContent ); liveSampleRouter.get("*", (_req: express.Request, res: express.Response) => res.status(404).send() diff --git a/gcp/function/src/env.ts b/gcp/function/src/env.ts index 541552efd75f..dd1cc0e35ba4 100644 --- a/gcp/function/src/env.ts +++ b/gcp/function/src/env.ts @@ -23,7 +23,6 @@ export enum RuntimeEnv { export enum Source { content = "content", - client = "client", liveSamples = "liveSamples", interactiveSamples = "interactiveSamples", bcdApi = "bcdApi", @@ -73,9 +72,6 @@ export const SOURCE_LIVE_SAMPLES: string = resolveSource( ); export const SOURCE_BCD_API: string = process.env["SOURCE_BCD_API"] || "https://bcd.developer.mozilla.org"; -export const SOURCE_CLIENT: string = resolveSource( - process.env["SOURCE_CLIENT"] || "https://developer.mozilla.org" -); export const SOURCE_INTERACTIVE_SAMPLES: string = resolveSource( process.env["SOURCE_INTERACTIVE_SAMPLES"] || "https://interactive-examples.mdn.mozilla.net" @@ -89,8 +85,6 @@ export function sourceUri(source: Source): string { return SOURCE_CONTENT; case Source.bcdApi: return SOURCE_BCD_API; - case Source.client: - return SOURCE_CLIENT; case Source.interactiveSamples: return SOURCE_INTERACTIVE_SAMPLES; case Source.liveSamples: diff --git a/gcp/function/src/handlers/client.ts b/gcp/function/src/handlers/client.ts deleted file mode 100644 index bd372efd5959..000000000000 --- a/gcp/function/src/handlers/client.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type express from "express"; -import httpProxy from "http-proxy"; -import * as path from "node:path"; -import { Source } from "../env.js"; -import { responder } from "../source.js"; -import { resolveIndexHTML } from "../utils.js"; -import { withResponseHeaders, withProxyResponseHeaders } from "../headers.js"; - -export function proxyClient(): express.Handler { - return responder({ - source: Source.client, - http(source) { - const clientProxy = httpProxy.createProxy({ - prependPath: true, - changeOrigin: true, - target: source, - autoRewrite: true, - }); - clientProxy.on("proxyRes", withProxyResponseHeaders); - return (req, res) => { - if (!req.url.startsWith("/static/")) { - req.url = resolveIndexHTML(req.url); - } - clientProxy.web(req, res); - }; - }, - file(source) { - return (req, res) => { - const resolvedPath = resolveIndexHTML(req.path); - const filePath = path.join(source, resolvedPath); - return withResponseHeaders(res, { - csp: resolvedPath.endsWith(".html"), - xFrame: true, - }).sendFile(filePath); - }; - }, - }); -} diff --git a/gcp/function/src/handlers/content.ts b/gcp/function/src/handlers/content.ts index 5def19b8a7e7..825ac6fc3610 100644 --- a/gcp/function/src/handlers/content.ts +++ b/gcp/function/src/handlers/content.ts @@ -12,23 +12,24 @@ export function createContentProxy(): express.Handler { http(source) { const contentProxy = httpProxy.createProxy({ prependPath: true, - ignorePath: true, changeOrigin: true, target: source, autoRewrite: true, }); - contentProxy.on("proxyReq", (proxyReq, req) => { - const resolvedPath = resolveIndexHTML(req.url || ""); - proxyReq.path = path.join(proxyReq.path, resolvedPath); - }); contentProxy.on("proxyRes", withProxyResponseHeaders); return (req, res) => { + if (!req.url.startsWith("/static/")) { + req.url = resolveIndexHTML(req.url); + } contentProxy.web(req, res); }; }, file(source) { return (req, res) => { - const resolvedPath = resolveIndexHTML(req.path); + let resolvedPath = req.path; + if (!resolvedPath.startsWith("/static/")) { + resolvedPath = resolveIndexHTML(resolvedPath); + } const filePath = path.join(source, resolvedPath); return withResponseHeaders(res, { csp: resolvedPath.endsWith(".html"), From 2985d3b88d57d279b2994bdb0ac66723762cdf26 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 21 Mar 2023 17:28:02 +0100 Subject: [PATCH 083/343] fix(gcp/function): add response headers if headers not sent --- gcp/function/src/headers.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gcp/function/src/headers.ts b/gcp/function/src/headers.ts index 6069c4600517..f0539cb7aff0 100644 --- a/gcp/function/src/headers.ts +++ b/gcp/function/src/headers.ts @@ -10,6 +10,10 @@ export function withProxyResponseHeaders( req: IncomingMessage, res: ServerResponse ): ServerResponse { + if (res.headersSent) { + return res; + } + const isLiveSampleURI = req.url?.includes("/_sample_.") ?? false; setResponseHeaders((name, value) => res.setHeader(name, value), { From c6653e3d71edaaf6cd2fed4838af0c0a2774427b Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 22 Mar 2023 10:55:21 +0100 Subject: [PATCH 084/343] chore(eslint): ignore copied libs --- .eslintignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintignore b/.eslintignore index 49908faa97a4..4e14d9da4f5a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,5 +7,6 @@ client/public/service-worker.js client/public/ client/src/document/*.js filecheck/*.js +gcp/function/src/internal/ mdn/content/ tool/*.js From 43ee2f1803e956bcbc2ed732dd6add295b5ac020 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 22 Mar 2023 22:05:57 +0100 Subject: [PATCH 085/343] chore(gcp/function): make libs copy read-only --- gcp/function/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/function/package.json b/gcp/function/package.json index 6e0e22960c64..1d5ee21c12f2 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -11,7 +11,7 @@ "build": "tsc -b && ts-node src/build.ts", "build-deploy-clean": "npm-run-all -s build deploy clean", "clean": "tsc -b --clean && git checkout -- redirects.json", - "copy-internal": "rm -rf ./src/internal && cp -R ../../libs ./src/internal", + "copy-internal": "rm -rf ./src/internal && cp -R ../../libs ./src/internal && chmod -R -w ./src/internal", "deploy": "gcloud functions deploy mdn-two --region europe-west3 --runtime nodejs18 --trigger-http --allow-unauthenticated", "package": "zip -r -X f.zip . -i package.json .env 'src/*.js' 'src/handlers/*.js' 'src/internal'", "prepare": "[ ! -e ../../libs ] || npm run copy-internal", From c8e29d97de6fc88b796f26e883e7eb99573600ec Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 22 Mar 2023 22:11:15 +0100 Subject: [PATCH 086/343] chore(gcp/function): expose plans at /plus/plans.json --- gcp/function/src/app.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 011712804df9..6032bcbe4175 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -14,6 +14,7 @@ const mainRouter = Router(); const proxyContent = createContentProxy(); mainRouter.get("/bcd/api/*", proxyBcdApi()); mainRouter.all("/api/v1/stripe/plans", stripePlans); +mainRouter.all("/plus/plans.json", stripePlans); mainRouter.all("/api/*", proxyRumba); mainRouter.all("/admin-api/*", proxyRumba); mainRouter.all("/events/fxa/*", proxyRumba); From 30e7cd6fe83fb7ec50c7fa0abee271f6b30f0147 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 22 Mar 2023 22:14:15 +0100 Subject: [PATCH 087/343] chore(gcp/function): remove file support --- gcp/function/src/env.ts | 21 +++--------- gcp/function/src/handlers/bcdApi.ts | 33 ++++++------------- gcp/function/src/handlers/content.ts | 48 ++++++++-------------------- gcp/function/src/source.ts | 16 ---------- 4 files changed, 28 insertions(+), 90 deletions(-) delete mode 100644 gcp/function/src/source.ts diff --git a/gcp/function/src/env.ts b/gcp/function/src/env.ts index dd1cc0e35ba4..419f14bbaca4 100644 --- a/gcp/function/src/env.ts +++ b/gcp/function/src/env.ts @@ -52,24 +52,11 @@ export function origin(req: express.Request): Origin { } } -function resolveSource(pathOrUrl: string) { - if (pathOrUrl.startsWith(".")) { - return path.resolve(path.join(cwd(), pathOrUrl)); - } else { - return pathOrUrl; - } -} - -export const SOURCE_CONTENT: string = resolveSource( - process.env["SOURCE_CONTENT"] || - process.env["BUILD_OUT_ROOT"] || - "https://developer.mozilla.org" -); -export const SOURCE_LIVE_SAMPLES: string = resolveSource( +export const SOURCE_CONTENT: string = + process.env["SOURCE_CONTENT"] || "https://developer.mozilla.org"; +export const SOURCE_LIVE_SAMPLES: string = process.env["SOURCE_LIVE_SAMPLES"] || - process.env["BUILD_OUT_ROOT"] || - "https://yari-demos.prod.mdn.mozit.cloud" -); + "https://yari-demos.prod.mdn.mozit.cloud"; export const SOURCE_BCD_API: string = process.env["SOURCE_BCD_API"] || "https://bcd.developer.mozilla.org"; export const SOURCE_INTERACTIVE_SAMPLES: string = resolveSource( diff --git a/gcp/function/src/handlers/bcdApi.ts b/gcp/function/src/handlers/bcdApi.ts index 6b779e3732e6..200917808e3d 100644 --- a/gcp/function/src/handlers/bcdApi.ts +++ b/gcp/function/src/handlers/bcdApi.ts @@ -1,29 +1,16 @@ import type * as express from "express"; import httpProxy from "http-proxy"; -import * as path from "node:path"; -import { Source } from "../env.js"; -import { responder } from "../source.js"; -import { withResponseHeaders, withProxyResponseHeaders } from "../headers.js"; +import { Source, sourceUri } from "../env.js"; +import { withProxyResponseHeaders } from "../headers.js"; export function proxyBcdApi(): express.Handler { - return responder({ - source: Source.bcdApi, - http(source) { - const bcdProxy = httpProxy.createProxy({ - prependPath: true, - changeOrigin: true, - target: source, - autoRewrite: true, - }); - bcdProxy.on("proxyRes", withProxyResponseHeaders); - return (req, res) => bcdProxy.web(req, res); - }, - file(source) { - return (req, res) => { - const rPath = req.path; - const filePath = path.join(source, rPath); - return withResponseHeaders(res).sendFile(filePath); - }; - }, + const bcdProxy = httpProxy.createProxy({ + prependPath: true, + changeOrigin: true, + target: sourceUri(Source.bcdApi), + autoRewrite: true, }); + bcdProxy.on("proxyRes", withProxyResponseHeaders); + + return (req, res) => bcdProxy.web(req, res); } diff --git a/gcp/function/src/handlers/content.ts b/gcp/function/src/handlers/content.ts index 825ac6fc3610..72daf2357a2a 100644 --- a/gcp/function/src/handlers/content.ts +++ b/gcp/function/src/handlers/content.ts @@ -1,41 +1,21 @@ import type express from "express"; -import * as path from "node:path"; -import { withResponseHeaders, withProxyResponseHeaders } from "../headers.js"; -import { responder } from "../source.js"; -import { Source } from "../env.js"; +import { withProxyResponseHeaders } from "../headers.js"; +import { Source, sourceUri } from "../env.js"; import httpProxy from "http-proxy"; import { resolveIndexHTML } from "../utils.js"; export function createContentProxy(): express.Handler { - return responder({ - source: Source.content, - http(source) { - const contentProxy = httpProxy.createProxy({ - prependPath: true, - changeOrigin: true, - target: source, - autoRewrite: true, - }); - contentProxy.on("proxyRes", withProxyResponseHeaders); - return (req, res) => { - if (!req.url.startsWith("/static/")) { - req.url = resolveIndexHTML(req.url); - } - contentProxy.web(req, res); - }; - }, - file(source) { - return (req, res) => { - let resolvedPath = req.path; - if (!resolvedPath.startsWith("/static/")) { - resolvedPath = resolveIndexHTML(resolvedPath); - } - const filePath = path.join(source, resolvedPath); - return withResponseHeaders(res, { - csp: resolvedPath.endsWith(".html"), - xFrame: true, - }).sendFile(filePath); - }; - }, + const contentProxy = httpProxy.createProxy({ + prependPath: true, + changeOrigin: true, + target: sourceUri(Source.content), + autoRewrite: true, }); + contentProxy.on("proxyRes", withProxyResponseHeaders); + return (req, res) => { + if (!req.url.startsWith("/static/")) { + req.url = resolveIndexHTML(req.url); + } + contentProxy.web(req, res); + }; } diff --git a/gcp/function/src/source.ts b/gcp/function/src/source.ts deleted file mode 100644 index ec0170f174e0..000000000000 --- a/gcp/function/src/source.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type express from "express"; -import { Source, sourceUri } from "./env.js"; - -export interface Transform { - source: Source; - http: (source: string) => express.Handler; - file: (source: string) => express.Handler; -} - -export function responder(transform: Transform): express.Handler { - const source = sourceUri(transform.source); - if (source.startsWith("http://") || source.startsWith("https://")) { - return transform.http(source); - } - return transform.file(source); -} From 28750f19dd9aa6a9bbcbbef096455cf5b7ffe677 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 22 Mar 2023 22:21:00 +0100 Subject: [PATCH 088/343] chore(gcp/function): remove unused sources --- gcp/function/src/env.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/gcp/function/src/env.ts b/gcp/function/src/env.ts index 419f14bbaca4..8226ff6fa98a 100644 --- a/gcp/function/src/env.ts +++ b/gcp/function/src/env.ts @@ -10,7 +10,6 @@ dotenv.config({ export enum Origin { main = "main", liveSamples = "liveSamples", - interactiveSamples = "interactiveSamples", unsafe = "unsafe", } @@ -24,9 +23,7 @@ export enum RuntimeEnv { export enum Source { content = "content", liveSamples = "liveSamples", - interactiveSamples = "interactiveSamples", bcdApi = "bcdApi", - bcdUpdates = "bcdUpdates", rumba = "rumba", } @@ -35,9 +32,6 @@ export const ORIGIN_MAIN: string = process.env["ORIGIN_MAIN"] || "developer.mozilla.org"; export const ORIGIN_LIVE_SAMPLES: string = process.env["ORIGIN_LIVE_SAMPLES"] || "yari-demos.prod.mdn.mozit.cloud"; -export const ORIGIN_INTERACTIVE_SAMPLES: string = - process.env["ORIGIN_INTERACTIVE_SAMPLES"] || - "interactive-examples.mdn.mozilla.net"; export function origin(req: express.Request): Origin { switch (req.hostname) { @@ -45,8 +39,6 @@ export function origin(req: express.Request): Origin { return Origin.main; case ORIGIN_LIVE_SAMPLES: return Origin.liveSamples; - case ORIGIN_INTERACTIVE_SAMPLES: - return Origin.interactiveSamples; default: return Origin.unsafe; } @@ -59,10 +51,6 @@ export const SOURCE_LIVE_SAMPLES: string = "https://yari-demos.prod.mdn.mozit.cloud"; export const SOURCE_BCD_API: string = process.env["SOURCE_BCD_API"] || "https://bcd.developer.mozilla.org"; -export const SOURCE_INTERACTIVE_SAMPLES: string = resolveSource( - process.env["SOURCE_INTERACTIVE_SAMPLES"] || - "https://interactive-examples.mdn.mozilla.net" -); export const SOURCE_RUMBA: string = process.env["SOURCE_RUMBA"] || "https://developer.mozilla.org"; @@ -72,8 +60,6 @@ export function sourceUri(source: Source): string { return SOURCE_CONTENT; case Source.bcdApi: return SOURCE_BCD_API; - case Source.interactiveSamples: - return SOURCE_INTERACTIVE_SAMPLES; case Source.liveSamples: return SOURCE_LIVE_SAMPLES; case Source.rumba: From 05784dd57b139139c9c4658d271e2f356a31ccd2 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 22 Mar 2023 22:28:05 +0100 Subject: [PATCH 089/343] chore(gcp/function): remove RuntimeEnv --- gcp/function/src/env.ts | 8 -------- gcp/function/src/headers.ts | 6 +----- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/gcp/function/src/env.ts b/gcp/function/src/env.ts index 8226ff6fa98a..884d846df8ce 100644 --- a/gcp/function/src/env.ts +++ b/gcp/function/src/env.ts @@ -13,13 +13,6 @@ export enum Origin { unsafe = "unsafe", } -export enum RuntimeEnv { - prod = "prod", - stage = "stage", - dev = "dev", - local = "local", -} - export enum Source { content = "content", liveSamples = "liveSamples", @@ -27,7 +20,6 @@ export enum Source { rumba = "rumba", } -export const RUNTIME_ENV: string = process.env["RUNTIME_ENV"] || "prod"; export const ORIGIN_MAIN: string = process.env["ORIGIN_MAIN"] || "developer.mozilla.org"; export const ORIGIN_LIVE_SAMPLES: string = diff --git a/gcp/function/src/headers.ts b/gcp/function/src/headers.ts index f0539cb7aff0..fcdb28090797 100644 --- a/gcp/function/src/headers.ts +++ b/gcp/function/src/headers.ts @@ -3,8 +3,6 @@ import type express from "express"; import { CSP_VALUE } from "@yari-internal/constants"; -import { RUNTIME_ENV } from "./env.js"; - export function withProxyResponseHeaders( _proxyRes: IncomingMessage, req: IncomingMessage, @@ -50,9 +48,7 @@ export function setResponseHeaders( ["X-XSS-Protection", "1; mode=block"], ["X-Content-Type-Options", "nosniff"], ["Strict-Transport-Security", "max-age=63072000"], - ...(csp && RUNTIME_ENV !== "local" - ? [["Content-Security-Policy", CSP_VALUE]] - : []), + ...(csp ? [["Content-Security-Policy", CSP_VALUE]] : []), ...(xFrame ? [["X-Frame-Options", "DENY"]] : []), ].forEach(([k, v]) => k && v && setHeader(k, v)); } From 9fd9c01195862eeebc2d64458a866b9efda0b861 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 22 Mar 2023 22:38:11 +0100 Subject: [PATCH 090/343] refactor(gcp/function): extract constants --- gcp/function/src/{handlers/cc2ip.ts => constants.ts} | 4 ++++ gcp/function/src/handlers/kevel.ts | 2 +- gcp/function/src/middlewares/content-origin-request.ts | 2 +- gcp/function/src/utils.ts | 3 +-- 4 files changed, 7 insertions(+), 4 deletions(-) rename gcp/function/src/{handlers/cc2ip.ts => constants.ts} (98%) diff --git a/gcp/function/src/handlers/cc2ip.ts b/gcp/function/src/constants.ts similarity index 98% rename from gcp/function/src/handlers/cc2ip.ts rename to gcp/function/src/constants.ts index e6b002a1cf19..0e3113ae2eb8 100644 --- a/gcp/function/src/handlers/cc2ip.ts +++ b/gcp/function/src/constants.ts @@ -171,3 +171,7 @@ export const CC_TO_IP: { [countryCode: string]: string } = { ZA: "102.36.123.181", ZW: "41.174.104.223", }; + +export const DEFAULT_COUNTRY = "US"; + +export const THIRTY_DAYS = 3600 * 24 * 30; diff --git a/gcp/function/src/handlers/kevel.ts b/gcp/function/src/handlers/kevel.ts index f05fff27ad02..1c53ebb2262d 100644 --- a/gcp/function/src/handlers/kevel.ts +++ b/gcp/function/src/handlers/kevel.ts @@ -11,7 +11,7 @@ import { CARBON_ZONE_KEY, CARBON_FALLBACK_ENABLED, } from "../env.js"; -import { CC_TO_IP } from "./cc2ip.js"; +import { CC_TO_IP } from "../constants.js"; import { getRequestCountry } from "../utils.js"; const siteId = KEVEL_SITE_ID; diff --git a/gcp/function/src/middlewares/content-origin-request.ts b/gcp/function/src/middlewares/content-origin-request.ts index 710e89a0ea39..c51d3536c514 100644 --- a/gcp/function/src/middlewares/content-origin-request.ts +++ b/gcp/function/src/middlewares/content-origin-request.ts @@ -6,11 +6,11 @@ import { resolveFundamental } from "@yari-internal/fundamental-redirects"; import { getLocale } from "@yari-internal/locale-utils"; import { decodePath } from "@yari-internal/slug-utils"; import { VALID_LOCALES } from "@yari-internal/constants"; +import { THIRTY_DAYS } from "../constants.js"; const require = createRequire(import.meta.url); const REDIRECTS = require("../../redirects.json"); const REDIRECT_SUFFIXES = ["/index.json", "/bcd.json", ""]; -const THIRTY_DAYS = 3600 * 24 * 30; const NEEDS_LOCALE = /^\/(?:docs|search|settings|signin|signup|plus)(?:$|\/)/; // Note that the keys of "VALID_LOCALES" are lowercase locales. const LOCALE_URI_WITHOUT_TRAILING_SLASH = new Set( diff --git a/gcp/function/src/utils.ts b/gcp/function/src/utils.ts index 1c1c8b87e6f7..f4625cee7735 100644 --- a/gcp/function/src/utils.ts +++ b/gcp/function/src/utils.ts @@ -1,6 +1,7 @@ import * as path from "node:path"; import type express from "express"; import { slugToFolder } from "@yari-internal/slug-utils"; +import { DEFAULT_COUNTRY } from "./constants.js"; export function resolveIndexHTML(pathOrUrl: string) { let resolvedPath = slugToFolder(pathOrUrl); @@ -10,8 +11,6 @@ export function resolveIndexHTML(pathOrUrl: string) { return resolvedPath; } -const DEFAULT_COUNTRY = "US"; - export function getRequestCountry(req: express.Request): string { // https://cloud.google.com/appengine/docs/flexible/reference/request-headers#app_engine-specific_headers const value = req.headers["x-appengine-country"]; From 8ffe9c6068934823485e377d75e43113daa9345f Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 22 Mar 2023 22:43:34 +0100 Subject: [PATCH 091/343] refactor(gcp/function): extract resolveIndexHTML middleware --- gcp/function/src/app.ts | 10 ++++++++-- gcp/function/src/handlers/content.ts | 4 ---- gcp/function/src/middlewares/resolveIndexHTML.ts | 16 ++++++++++++++++ gcp/function/src/utils.ts | 10 ---------- 4 files changed, 24 insertions(+), 16 deletions(-) create mode 100644 gcp/function/src/middlewares/resolveIndexHTML.ts diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 6032bcbe4175..27151488a938 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -9,6 +9,7 @@ import { stripePlans } from "./handlers/stripePlans.js"; import { proxyTelemetry } from "./handlers/telemetry.js"; import { pathnameLC } from "./middlewares/pathnameLC.js"; import { contentOriginRequest } from "./middlewares/content-origin-request.js"; +import { resolveIndexHTML } from "./middlewares/resolveIndexHTML.js"; const mainRouter = Router(); const proxyContent = createContentProxy(); @@ -22,9 +23,14 @@ mainRouter.all("/users/fxa/*", proxyRumba); mainRouter.all("/submit/mdn-yari/*", proxyTelemetry); mainRouter.all("/pong/*", express.json(), proxyKevel); mainRouter.all("/pimg/*", proxyKevel); -mainRouter.get("/[^/]+/docs/*", contentOriginRequest, proxyContent); +mainRouter.get( + "/[^/]+/docs/*", + contentOriginRequest, + resolveIndexHTML, + proxyContent +); mainRouter.get("/[^/]+/search-index.json", contentOriginRequest, proxyContent); -mainRouter.get("*", contentOriginRequest, proxyContent); +mainRouter.get("*", contentOriginRequest, resolveIndexHTML, proxyContent); const liveSampleRouter = Router(); liveSampleRouter.use(pathnameLC); diff --git a/gcp/function/src/handlers/content.ts b/gcp/function/src/handlers/content.ts index 72daf2357a2a..8beb8e5f6eb9 100644 --- a/gcp/function/src/handlers/content.ts +++ b/gcp/function/src/handlers/content.ts @@ -2,7 +2,6 @@ import type express from "express"; import { withProxyResponseHeaders } from "../headers.js"; import { Source, sourceUri } from "../env.js"; import httpProxy from "http-proxy"; -import { resolveIndexHTML } from "../utils.js"; export function createContentProxy(): express.Handler { const contentProxy = httpProxy.createProxy({ @@ -13,9 +12,6 @@ export function createContentProxy(): express.Handler { }); contentProxy.on("proxyRes", withProxyResponseHeaders); return (req, res) => { - if (!req.url.startsWith("/static/")) { - req.url = resolveIndexHTML(req.url); - } contentProxy.web(req, res); }; } diff --git a/gcp/function/src/middlewares/resolveIndexHTML.ts b/gcp/function/src/middlewares/resolveIndexHTML.ts new file mode 100644 index 000000000000..59acae9189c7 --- /dev/null +++ b/gcp/function/src/middlewares/resolveIndexHTML.ts @@ -0,0 +1,16 @@ +import type express from "express"; +import { slugToFolder } from "@yari-internal/slug-utils"; +import * as path from "node:path"; + +export function resolveIndexHTML( + req: express.Request, + _res: express.Response, + next: express.NextFunction +) { + let resolvedUrl = slugToFolder(req.url); + if (path.extname(resolvedUrl) === "") { + resolvedUrl = path.join(resolvedUrl, "index.html"); + } + req.url = resolvedUrl; + next(); +} diff --git a/gcp/function/src/utils.ts b/gcp/function/src/utils.ts index f4625cee7735..f456746dda2e 100644 --- a/gcp/function/src/utils.ts +++ b/gcp/function/src/utils.ts @@ -1,16 +1,6 @@ -import * as path from "node:path"; import type express from "express"; -import { slugToFolder } from "@yari-internal/slug-utils"; import { DEFAULT_COUNTRY } from "./constants.js"; -export function resolveIndexHTML(pathOrUrl: string) { - let resolvedPath = slugToFolder(pathOrUrl); - if (path.extname(resolvedPath) === "") { - resolvedPath = path.join(resolvedPath, "index.html"); - } - return resolvedPath; -} - export function getRequestCountry(req: express.Request): string { // https://cloud.google.com/appengine/docs/flexible/reference/request-headers#app_engine-specific_headers const value = req.headers["x-appengine-country"]; From 8729385a5ea71ced3aaa795309b561e4f7c3af2c Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 22 Mar 2023 22:46:12 +0100 Subject: [PATCH 092/343] fix(gcp/function): avoid that plans are cached in CDN --- gcp/function/src/handlers/stripePlans.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/gcp/function/src/handlers/stripePlans.ts b/gcp/function/src/handlers/stripePlans.ts index 1a673f629c07..87f611494bb3 100644 --- a/gcp/function/src/handlers/stripePlans.ts +++ b/gcp/function/src/handlers/stripePlans.ts @@ -109,9 +109,12 @@ export function stripePlans(req: express.Request, res: express.Response) { plans: planResult, } satisfies Result; - return res - .status(200) - .setHeader("Cache-Control", "max-age=86400") - .setHeader("Content-Type", "application/json") - .end(JSON.stringify(result)); + return ( + res + .status(200) + // Google CDN cannot partition by country, so we can only cache in browser. + .setHeader("Cache-Control", "private, max-age=86400") + .setHeader("Content-Type", "application/json") + .end(JSON.stringify(result)) + ); } From 2919b59d6a9e40a595b710f6e662dac6b5b2a3f5 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 22 Mar 2023 22:49:05 +0100 Subject: [PATCH 093/343] refactor(gcp/function): rename stripePlans => plans --- gcp/function/src/app.ts | 6 +++--- gcp/function/src/handlers/{stripePlans.ts => plans.ts} | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename gcp/function/src/handlers/{stripePlans.ts => plans.ts} (97%) diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 27151488a938..67ea344847b5 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -5,7 +5,7 @@ import { createContentProxy } from "./handlers/content.js"; import { proxyBcdApi } from "./handlers/bcdApi.js"; import { proxyKevel } from "./handlers/kevel.js"; import { proxyRumba } from "./handlers/rumba.js"; -import { stripePlans } from "./handlers/stripePlans.js"; +import { plans } from "./handlers/plans.js"; import { proxyTelemetry } from "./handlers/telemetry.js"; import { pathnameLC } from "./middlewares/pathnameLC.js"; import { contentOriginRequest } from "./middlewares/content-origin-request.js"; @@ -14,8 +14,8 @@ import { resolveIndexHTML } from "./middlewares/resolveIndexHTML.js"; const mainRouter = Router(); const proxyContent = createContentProxy(); mainRouter.get("/bcd/api/*", proxyBcdApi()); -mainRouter.all("/api/v1/stripe/plans", stripePlans); -mainRouter.all("/plus/plans.json", stripePlans); +mainRouter.all("/api/v1/stripe/plans", plans); +mainRouter.all("/plus/plans.json", plans); mainRouter.all("/api/*", proxyRumba); mainRouter.all("/admin-api/*", proxyRumba); mainRouter.all("/events/fxa/*", proxyRumba); diff --git a/gcp/function/src/handlers/stripePlans.ts b/gcp/function/src/handlers/plans.ts similarity index 97% rename from gcp/function/src/handlers/stripePlans.ts rename to gcp/function/src/handlers/plans.ts index 87f611494bb3..cc02f5658321 100644 --- a/gcp/function/src/handlers/stripePlans.ts +++ b/gcp/function/src/handlers/plans.ts @@ -56,7 +56,7 @@ interface Result { plans: PlanResult; } -export function stripePlans(req: express.Request, res: express.Response) { +export function plans(req: express.Request, res: express.Response) { const lookupData: LookupData = ORIGIN_MAIN === "developer.mozilla.org" ? prodLookup : stageLookup; From 8237f162b676d8f378b089d4c9e24f3d004d0650 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 22 Mar 2023 23:26:08 +0100 Subject: [PATCH 094/343] refactor(gcp/function): extract redirect() helper --- gcp/function/src/utils.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/gcp/function/src/utils.ts b/gcp/function/src/utils.ts index f456746dda2e..6429b2186d07 100644 --- a/gcp/function/src/utils.ts +++ b/gcp/function/src/utils.ts @@ -11,3 +11,30 @@ export function getRequestCountry(req: express.Request): string { return DEFAULT_COUNTRY; } } + +export function redirect( + res: express.Response, + location: string, + { status = 302, cacheControlSeconds = 0 } = {} +): void { + let cacheControlValue; + if (cacheControlSeconds) { + cacheControlValue = `max-age=${cacheControlSeconds},public`; + } else { + cacheControlValue = "no-store"; + } + + // We need to URL encode the pathname, but leave the query string as is. + // Suppose the old URL was `/search?q=text%2Dshadow` and all we need to do + // is to inject the locale to that URL, we should not URL encode the whole + // new URL otherwise you'd end up with `/en-US/search?q=text%252Dshadow` + // since the already encoded `%2D` would become `%252D` which is wrong and + // different. + const [pathname, querystring] = location.split("?", 2); + let newLocation = encodeURI(pathname || ""); + if (querystring) { + newLocation += `?${querystring}`; + } + + res.set("Cache-Control", cacheControlValue).redirect(status, newLocation); +} From 0dc9b453acb84ef88d0d14b0e7efc1e1f835e6ac Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 22 Mar 2023 22:57:48 +0100 Subject: [PATCH 095/343] refactor(gcp/function): extract global redirectLeadingDoubleSlash middleware --- gcp/function/src/app.ts | 2 ++ .../src/middlewares/content-origin-request.ts | 16 ----------- .../src/middlewares/redirectLeadingSlash.ts | 27 +++++++++++++++++++ 3 files changed, 29 insertions(+), 16 deletions(-) create mode 100644 gcp/function/src/middlewares/redirectLeadingSlash.ts diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 67ea344847b5..d5e6f0c42a5c 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -10,9 +10,11 @@ import { proxyTelemetry } from "./handlers/telemetry.js"; import { pathnameLC } from "./middlewares/pathnameLC.js"; import { contentOriginRequest } from "./middlewares/content-origin-request.js"; import { resolveIndexHTML } from "./middlewares/resolveIndexHTML.js"; +import { redirectLeadingSlash } from "./middlewares/redirectLeadingSlash.js"; const mainRouter = Router(); const proxyContent = createContentProxy(); +mainRouter.use(redirectLeadingSlash); mainRouter.get("/bcd/api/*", proxyBcdApi()); mainRouter.all("/api/v1/stripe/plans", plans); mainRouter.all("/plus/plans.json", plans); diff --git a/gcp/function/src/middlewares/content-origin-request.ts b/gcp/function/src/middlewares/content-origin-request.ts index c51d3536c514..36ff97a81910 100644 --- a/gcp/function/src/middlewares/content-origin-request.ts +++ b/gcp/function/src/middlewares/content-origin-request.ts @@ -72,22 +72,6 @@ export function contentOriginRequest( const requestURILowerCase = requestURI.toLowerCase(); const qs = url.search; - // If the URL was something like `https://domain/en-US/search/`, our code - // would make a that a redirect to `/en-US/search` (stripping the trailing slash). - // But if it was `https://domain//en-US/search/` it *would* make a redirect - // to `//en-US/search`. - // However, if pathname starts with `//` the Location header might look - // relative but it's actually an absolute URL. - // A 302 redirect from `https://domain//evil.com/` actually ends open - // opening `https://evil.com/` in the browser, because the browser will - // treat `//evil.com/ == https://evil.com/`. - // Prevent any pathnames that start with a double //. - // This essentially means that a request for `GET /////anything` becomes - // 302 with `Location: /anything`. - if (requestURI.startsWith("//")) { - return redirect(`/${requestURI.replace(/^\/+/g, "")}`); - } - const fundamentalRedirect = resolveFundamental(requestURI); if (fundamentalRedirect.url) { // NOTE: The query string is not forwarded for document requests, diff --git a/gcp/function/src/middlewares/redirectLeadingSlash.ts b/gcp/function/src/middlewares/redirectLeadingSlash.ts new file mode 100644 index 000000000000..6f9d30aa81da --- /dev/null +++ b/gcp/function/src/middlewares/redirectLeadingSlash.ts @@ -0,0 +1,27 @@ +import type express from "express"; +import { redirect } from "../utils.js"; + +// If the URL was something like `https://domain/en-US/search/`, our code +// would make a that a redirect to `/en-US/search` (stripping the trailing slash). +// But if it was `https://domain//en-US/search/` it *would* make a redirect +// to `//en-US/search`. +// However, if pathname starts with `//` the Location header might look +// relative but it's actually an absolute URL. +// A 302 redirect from `https://domain//evil.com/` actually ends open +// opening `https://evil.com/` in the browser, because the browser will +// treat `//evil.com/ == https://evil.com/`. +// Prevent any pathnames that start with a double //. +// This essentially means that a request for `GET /////anything` becomes +// 302 with `Location: /anything`. +export function redirectLeadingSlash( + req: express.Request, + res: express.Response, + next: express.NextFunction +) { + const pathname = req.url; + if (pathname.startsWith("//")) { + return redirect(res, pathname.replace(/^\/+/g, "/")); + } + + next(); +} From 985940b1eea69a4ee0890b4b4a95ef0bc428a9a9 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 22 Mar 2023 23:06:35 +0100 Subject: [PATCH 096/343] refactor(gcp/function): extract redirectMovedPages middleware --- gcp/function/src/app.ts | 2 + .../src/middlewares/content-origin-request.ts | 34 -------------- .../src/middlewares/redirectMovedPages.ts | 47 +++++++++++++++++++ 3 files changed, 49 insertions(+), 34 deletions(-) create mode 100644 gcp/function/src/middlewares/redirectMovedPages.ts diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index d5e6f0c42a5c..dc62f11d1bb6 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -11,6 +11,7 @@ import { pathnameLC } from "./middlewares/pathnameLC.js"; import { contentOriginRequest } from "./middlewares/content-origin-request.js"; import { resolveIndexHTML } from "./middlewares/resolveIndexHTML.js"; import { redirectLeadingSlash } from "./middlewares/redirectLeadingSlash.js"; +import { redirectMovedPages } from "./middlewares/redirectMovedPages.js"; const mainRouter = Router(); const proxyContent = createContentProxy(); @@ -28,6 +29,7 @@ mainRouter.all("/pimg/*", proxyKevel); mainRouter.get( "/[^/]+/docs/*", contentOriginRequest, + redirectMovedPages, resolveIndexHTML, proxyContent ); diff --git a/gcp/function/src/middlewares/content-origin-request.ts b/gcp/function/src/middlewares/content-origin-request.ts index 36ff97a81910..5f1e8fdda4b5 100644 --- a/gcp/function/src/middlewares/content-origin-request.ts +++ b/gcp/function/src/middlewares/content-origin-request.ts @@ -1,16 +1,10 @@ -import { createRequire } from "node:module"; - import type express from "express"; import { resolveFundamental } from "@yari-internal/fundamental-redirects"; import { getLocale } from "@yari-internal/locale-utils"; -import { decodePath } from "@yari-internal/slug-utils"; import { VALID_LOCALES } from "@yari-internal/constants"; import { THIRTY_DAYS } from "../constants.js"; -const require = createRequire(import.meta.url); -const REDIRECTS = require("../../redirects.json"); -const REDIRECT_SUFFIXES = ["/index.json", "/bcd.json", ""]; const NEEDS_LOCALE = /^\/(?:docs|search|settings|signin|signup|plus)(?:$|\/)/; // Note that the keys of "VALID_LOCALES" are lowercase locales. const LOCALE_URI_WITHOUT_TRAILING_SLASH = new Set( @@ -153,33 +147,5 @@ export function contentOriginRequest( }); } - // Important: The requestURI may be URI-encoded. - // Example: - // - Encoded: /zh-TW/docs/AJAX:%E4%B8%8A%E6%89%8B%E7%AF%87 - // - Decoded: /zh-TW/docs/AJAX:上手篇 - const decodedUri = decodePath(url.pathname); - const decodedUriLC = decodedUri.toLowerCase(); - - // Redirect moved pages (see `_redirects.txt` in content/translated-content). - // Example: - // - Source: /zh-TW/docs/AJAX:上手篇 - // - Target: /zh-TW/docs/Web/Guide/AJAX/Getting_Started - for (const suffix of REDIRECT_SUFFIXES) { - if (!decodedUriLC.endsWith(suffix)) { - continue; - } - const source = decodedUriLC.substring( - 0, - decodedUriLC.length - suffix.length - ); - if (typeof REDIRECTS[source] == "string") { - const target = REDIRECTS[source] + suffix; - return redirect(target, { - status: 301, - cacheControlSeconds: THIRTY_DAYS, - }); - } - } - next(); } diff --git a/gcp/function/src/middlewares/redirectMovedPages.ts b/gcp/function/src/middlewares/redirectMovedPages.ts new file mode 100644 index 000000000000..9d85b11d2df0 --- /dev/null +++ b/gcp/function/src/middlewares/redirectMovedPages.ts @@ -0,0 +1,47 @@ +import { createRequire } from "node:module"; + +import type express from "express"; +import { decodePath } from "@yari-internal/slug-utils"; + +import { THIRTY_DAYS } from "../constants.js"; +import { redirect } from "../utils.js"; + +const require = createRequire(import.meta.url); +const REDIRECTS = require("../../redirects.json"); +const REDIRECT_SUFFIXES = ["/index.json", "/bcd.json", ""]; + +export function redirectMovedPages( + req: express.Request, + res: express.Response, + next: express.NextFunction +) { + // Important: The requestURI may be URI-encoded. + // Example: + // - Encoded: /zh-TW/docs/AJAX:%E4%B8%8A%E6%89%8B%E7%AF%87 + // - Decoded: /zh-TW/docs/AJAX:上手篇 + const decodedUri = decodePath(req.path); + const decodedUriLC = decodedUri.toLowerCase(); + + // Redirect moved pages (see `_redirects.txt` in content/translated-content). + // Example: + // - Source: /zh-TW/docs/AJAX:上手篇 + // - Target: /zh-TW/docs/Web/Guide/AJAX/Getting_Started + for (const suffix of REDIRECT_SUFFIXES) { + if (!decodedUriLC.endsWith(suffix)) { + continue; + } + const source = decodedUriLC.substring( + 0, + decodedUriLC.length - suffix.length + ); + if (typeof REDIRECTS[source] == "string") { + const target = REDIRECTS[source] + suffix; + return redirect(res, target, { + status: 301, + cacheControlSeconds: THIRTY_DAYS, + }); + } + } + + next(); +} From 39e208dafefd77308db562fa280b18aac8a05bc9 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 22 Mar 2023 23:11:37 +0100 Subject: [PATCH 097/343] refactor(gcp/function): extract redirectFundamental middleware --- gcp/function/src/app.ts | 10 +++++- .../src/middlewares/content-origin-request.ts | 17 ---------- .../src/middlewares/redirectFundamental.ts | 32 +++++++++++++++++++ 3 files changed, 41 insertions(+), 18 deletions(-) create mode 100644 gcp/function/src/middlewares/redirectFundamental.ts diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index dc62f11d1bb6..a38bc0eb1e5d 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -12,6 +12,7 @@ import { contentOriginRequest } from "./middlewares/content-origin-request.js"; import { resolveIndexHTML } from "./middlewares/resolveIndexHTML.js"; import { redirectLeadingSlash } from "./middlewares/redirectLeadingSlash.js"; import { redirectMovedPages } from "./middlewares/redirectMovedPages.js"; +import { redirectFundamental } from "./middlewares/redirectFundamental.js"; const mainRouter = Router(); const proxyContent = createContentProxy(); @@ -28,13 +29,20 @@ mainRouter.all("/pong/*", express.json(), proxyKevel); mainRouter.all("/pimg/*", proxyKevel); mainRouter.get( "/[^/]+/docs/*", + redirectFundamental, contentOriginRequest, redirectMovedPages, resolveIndexHTML, proxyContent ); mainRouter.get("/[^/]+/search-index.json", contentOriginRequest, proxyContent); -mainRouter.get("*", contentOriginRequest, resolveIndexHTML, proxyContent); +mainRouter.get( + "*", + redirectFundamental, + contentOriginRequest, + resolveIndexHTML, + proxyContent +); const liveSampleRouter = Router(); liveSampleRouter.use(pathnameLC); diff --git a/gcp/function/src/middlewares/content-origin-request.ts b/gcp/function/src/middlewares/content-origin-request.ts index 5f1e8fdda4b5..3cd5c16e4b9e 100644 --- a/gcp/function/src/middlewares/content-origin-request.ts +++ b/gcp/function/src/middlewares/content-origin-request.ts @@ -1,6 +1,5 @@ import type express from "express"; -import { resolveFundamental } from "@yari-internal/fundamental-redirects"; import { getLocale } from "@yari-internal/locale-utils"; import { VALID_LOCALES } from "@yari-internal/constants"; import { THIRTY_DAYS } from "../constants.js"; @@ -66,22 +65,6 @@ export function contentOriginRequest( const requestURILowerCase = requestURI.toLowerCase(); const qs = url.search; - const fundamentalRedirect = resolveFundamental(requestURI); - if (fundamentalRedirect.url) { - // NOTE: The query string is not forwarded for document requests, - // as directed by their origin request policy, so it's safe to - // assume "request.querystring" is empty for document requests. - if (url.search) { - fundamentalRedirect.url += - (fundamentalRedirect.url.includes("?") ? "&" : "?") + - url.search.substring(1); - } - return redirect(fundamentalRedirect.url, { - status: fundamentalRedirect.status, - cacheControlSeconds: THIRTY_DAYS, - }); - } - // Do we need to insert the locale? If we do, trim a trailing slash // to avoid a double redirect, except when requesting the home page. if ( diff --git a/gcp/function/src/middlewares/redirectFundamental.ts b/gcp/function/src/middlewares/redirectFundamental.ts new file mode 100644 index 000000000000..83f1eeff2498 --- /dev/null +++ b/gcp/function/src/middlewares/redirectFundamental.ts @@ -0,0 +1,32 @@ +import type express from "express"; + +import { THIRTY_DAYS } from "../constants.js"; +import { resolveFundamental } from "@yari-internal/fundamental-redirects"; +import { redirect } from "../utils.js"; + +export function redirectFundamental( + req: express.Request, + res: express.Response, + next: express.NextFunction +) { + const url = new URL(req.url, `${req.protocol}://${req.headers.host}`); + const requestURI = req.path; + + const fundamentalRedirect = resolveFundamental(requestURI); + if (fundamentalRedirect.url) { + // NOTE: The query string is not forwarded for document requests, + // as directed by their origin request policy, so it's safe to + // assume "request.querystring" is empty for document requests. + if (url.search) { + fundamentalRedirect.url += + (fundamentalRedirect.url.includes("?") ? "&" : "?") + + url.search.substring(1); + } + redirect(res, fundamentalRedirect.url, { + status: fundamentalRedirect.status, + cacheControlSeconds: THIRTY_DAYS, + }); + } + + next(); +} From 3f9a1576a040a43541df57a5bd86394fbbd37589 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 22 Mar 2023 23:16:14 +0100 Subject: [PATCH 098/343] refactor(gcp/function): extract redirectLocale middleware --- gcp/function/src/app.ts | 3 ++ .../src/middlewares/content-origin-request.ts | 36 ------------- .../src/middlewares/redirectLocale.ts | 54 +++++++++++++++++++ 3 files changed, 57 insertions(+), 36 deletions(-) create mode 100644 gcp/function/src/middlewares/redirectLocale.ts diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index a38bc0eb1e5d..637d948e7f8e 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -13,6 +13,7 @@ import { resolveIndexHTML } from "./middlewares/resolveIndexHTML.js"; import { redirectLeadingSlash } from "./middlewares/redirectLeadingSlash.js"; import { redirectMovedPages } from "./middlewares/redirectMovedPages.js"; import { redirectFundamental } from "./middlewares/redirectFundamental.js"; +import { redirectLocale } from "./middlewares/redirectLocale.js"; const mainRouter = Router(); const proxyContent = createContentProxy(); @@ -30,6 +31,7 @@ mainRouter.all("/pimg/*", proxyKevel); mainRouter.get( "/[^/]+/docs/*", redirectFundamental, + redirectLocale, contentOriginRequest, redirectMovedPages, resolveIndexHTML, @@ -39,6 +41,7 @@ mainRouter.get("/[^/]+/search-index.json", contentOriginRequest, proxyContent); mainRouter.get( "*", redirectFundamental, + redirectLocale, contentOriginRequest, resolveIndexHTML, proxyContent diff --git a/gcp/function/src/middlewares/content-origin-request.ts b/gcp/function/src/middlewares/content-origin-request.ts index 3cd5c16e4b9e..0655b6921138 100644 --- a/gcp/function/src/middlewares/content-origin-request.ts +++ b/gcp/function/src/middlewares/content-origin-request.ts @@ -1,10 +1,8 @@ import type express from "express"; -import { getLocale } from "@yari-internal/locale-utils"; import { VALID_LOCALES } from "@yari-internal/constants"; import { THIRTY_DAYS } from "../constants.js"; -const NEEDS_LOCALE = /^\/(?:docs|search|settings|signin|signup|plus)(?:$|\/)/; // Note that the keys of "VALID_LOCALES" are lowercase locales. const LOCALE_URI_WITHOUT_TRAILING_SLASH = new Set( [...VALID_LOCALES.keys()].map((locale) => `/${locale}`) @@ -65,40 +63,6 @@ export function contentOriginRequest( const requestURILowerCase = requestURI.toLowerCase(); const qs = url.search; - // Do we need to insert the locale? If we do, trim a trailing slash - // to avoid a double redirect, except when requesting the home page. - if ( - requestURI === "" || - requestURI === "/" || - NEEDS_LOCALE.test(requestURILowerCase) - ) { - const path = requestURI.endsWith("/") - ? requestURI.slice(0, -1) - : requestURI; - // Note that "getLocale" only returns valid locales, never a retired locale. - const locale = getLocale(req); - // The only time we actually want a trailing slash is when the URL is just - // the locale. E.g. `/en-US/` (not `/en-US`) - return redirect(`/${locale}${path || "/"}` + qs); - } - - // At this point, the URI is guaranteed to start with a forward slash. - const uriParts = requestURI.split("/"); - const uriFirstPart = uriParts[1] ?? ""; - const uriFirstPartLC = uriFirstPart.toLowerCase(); - - // Do we need to redirect to the properly-cased locale? We also ensure - // here that requests for the home page have a trailing slash, while - // all others do not. - if ( - VALID_LOCALES.has(uriFirstPartLC) && - uriFirstPart !== VALID_LOCALES.get(uriFirstPartLC) - ) { - // Assemble the rest of the path without a trailing slash. - const extra = uriParts.slice(2).filter(Boolean).join("/"); - return redirect(`/${VALID_LOCALES.get(uriFirstPartLC)}/${extra}${qs}`); - } - // Handle cases related to the presence or absence of a trailing-slash. if (LOCALE_URI_WITHOUT_TRAILING_SLASH.has(requestURILowerCase)) { // Home page requests are the special case on MDN. They should diff --git a/gcp/function/src/middlewares/redirectLocale.ts b/gcp/function/src/middlewares/redirectLocale.ts new file mode 100644 index 000000000000..83aab399ed74 --- /dev/null +++ b/gcp/function/src/middlewares/redirectLocale.ts @@ -0,0 +1,54 @@ +import type express from "express"; + +import { getLocale } from "@yari-internal/locale-utils"; +import { VALID_LOCALES } from "@yari-internal/constants"; +import { redirect } from "../utils.js"; + +const NEEDS_LOCALE = /^\/(?:docs|search|settings|signin|signup|plus)(?:$|\/)/; + +export function redirectLocale( + req: express.Request, + res: express.Response, + next: express.NextFunction +) { + const url = new URL(req.url, `${req.protocol}://${req.headers.host}`); + const requestURI = url.pathname; + const requestURILowerCase = requestURI.toLowerCase(); + const qs = url.search; + + // Do we need to insert the locale? If we do, trim a trailing slash + // to avoid a double redirect, except when requesting the home page. + if ( + requestURI === "" || + requestURI === "/" || + NEEDS_LOCALE.test(requestURILowerCase) + ) { + const path = requestURI.endsWith("/") + ? requestURI.slice(0, -1) + : requestURI; + // Note that "getLocale" only returns valid locales, never a retired locale. + const locale = getLocale(req); + // The only time we actually want a trailing slash is when the URL is just + // the locale. E.g. `/en-US/` (not `/en-US`) + return redirect(res, `/${locale}${path || "/"}` + qs); + } + + // At this point, the URI is guaranteed to start with a forward slash. + const uriParts = requestURI.split("/"); + const uriFirstPart = uriParts[1] ?? ""; + const uriFirstPartLC = uriFirstPart.toLowerCase(); + + // Do we need to redirect to the properly-cased locale? We also ensure + // here that requests for the home page have a trailing slash, while + // all others do not. + if ( + VALID_LOCALES.has(uriFirstPartLC) && + uriFirstPart !== VALID_LOCALES.get(uriFirstPartLC) + ) { + // Assemble the rest of the path without a trailing slash. + const extra = uriParts.slice(2).filter(Boolean).join("/"); + return redirect(res, `/${VALID_LOCALES.get(uriFirstPartLC)}/${extra}${qs}`); + } + + next(); +} From 3175dbbcccfad20c67ab525a0fb9bc8a44ca4fee Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 22 Mar 2023 23:22:05 +0100 Subject: [PATCH 099/343] refactor(gcp/function): extract redirectTrailingSlash middleware --- gcp/function/src/app.ts | 3 + .../src/middlewares/content-origin-request.ts | 60 ---------------- .../src/middlewares/redirectTrailingSlash.ts | 70 +++++++++++++++++++ 3 files changed, 73 insertions(+), 60 deletions(-) create mode 100644 gcp/function/src/middlewares/redirectTrailingSlash.ts diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 637d948e7f8e..0e8ca7db3df1 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -14,6 +14,7 @@ import { redirectLeadingSlash } from "./middlewares/redirectLeadingSlash.js"; import { redirectMovedPages } from "./middlewares/redirectMovedPages.js"; import { redirectFundamental } from "./middlewares/redirectFundamental.js"; import { redirectLocale } from "./middlewares/redirectLocale.js"; +import { redirectTrailingSlash } from "./middlewares/redirectTrailingSlash.js"; const mainRouter = Router(); const proxyContent = createContentProxy(); @@ -32,6 +33,7 @@ mainRouter.get( "/[^/]+/docs/*", redirectFundamental, redirectLocale, + redirectTrailingSlash, contentOriginRequest, redirectMovedPages, resolveIndexHTML, @@ -42,6 +44,7 @@ mainRouter.get( "*", redirectFundamental, redirectLocale, + redirectTrailingSlash, contentOriginRequest, resolveIndexHTML, proxyContent diff --git a/gcp/function/src/middlewares/content-origin-request.ts b/gcp/function/src/middlewares/content-origin-request.ts index 0655b6921138..243ecf3e095b 100644 --- a/gcp/function/src/middlewares/content-origin-request.ts +++ b/gcp/function/src/middlewares/content-origin-request.ts @@ -1,29 +1,5 @@ import type express from "express"; -import { VALID_LOCALES } from "@yari-internal/constants"; -import { THIRTY_DAYS } from "../constants.js"; - -// Note that the keys of "VALID_LOCALES" are lowercase locales. -const LOCALE_URI_WITHOUT_TRAILING_SLASH = new Set( - [...VALID_LOCALES.keys()].map((locale) => `/${locale}`) -); -const LOCALE_URI_WITH_TRAILING_SLASH = new Set( - [...VALID_LOCALES.keys()].map((locale) => `/${locale}/`) -); -// TODO: The code that uses LEGACY_URI_NEEDING_TRAILING_SLASH should be -// temporary. For example, when we have moved to the Yari-built -// account settings page, we should add fundamental redirects -// for "/{locale}/account/?" and "/account/?" that redirect to -// "/{locale}/settings" and "/settings" respectively. The other -// cases can be either redirected or deleted eventually as well. -// The goal is to eventually remove the code that uses -// LEGACY_URI_NEEDING_TRAILING_SLASH. -const LEGACY_URI_NEEDING_TRAILING_SLASH = new RegExp( - `^(?:${[...LOCALE_URI_WITHOUT_TRAILING_SLASH].join( - "|" - )})?/(?:account|contribute|maintenance-mode|payments)/?$` -); - export function contentOriginRequest( req: express.Request, res: express.Response, @@ -58,41 +34,5 @@ export function contentOriginRequest( next(); } - const url = new URL(req.url, `${req.protocol}://${req.headers.host}`); - let requestURI = url.pathname; - const requestURILowerCase = requestURI.toLowerCase(); - const qs = url.search; - - // Handle cases related to the presence or absence of a trailing-slash. - if (LOCALE_URI_WITHOUT_TRAILING_SLASH.has(requestURILowerCase)) { - // Home page requests are the special case on MDN. They should - // always have a trailing slash. So a home page URL without a - // trailing slash should redirect to the same URL with a - // trailing slash. When the redirected home-page request is - // processed by this Lambda function, note that we'll remove - // the trailing slash before the request reaches S3 (see below). - return redirect(requestURI + "/" + qs, { - cacheControlSeconds: THIRTY_DAYS, - }); - } else if (LOCALE_URI_WITH_TRAILING_SLASH.has(requestURILowerCase)) { - // We've received a proper request for a locale's home page (i.e., - // it has a traling slash), but since that request will be served - // from S3, we need to strip the trailing slash before it reaches - // S3. This is required because we store the home pages in S3 as - // their path name itself, for example "en-us" for the English home - // page, not "en-us/index.html", which is what S3 would look for if - // we left the trailing slash. - requestURI = requestURI.slice(0, -1); - } else if ( - requestURI.endsWith("/") && - !LEGACY_URI_NEEDING_TRAILING_SLASH.test(requestURILowerCase) - ) { - // All other requests with a trailing slash should redirect to the - // same URL without the trailing slash. - return redirect(requestURI.slice(0, -1) + qs, { - cacheControlSeconds: THIRTY_DAYS, - }); - } - next(); } diff --git a/gcp/function/src/middlewares/redirectTrailingSlash.ts b/gcp/function/src/middlewares/redirectTrailingSlash.ts new file mode 100644 index 000000000000..660130ef9497 --- /dev/null +++ b/gcp/function/src/middlewares/redirectTrailingSlash.ts @@ -0,0 +1,70 @@ +import type express from "express"; + +import { THIRTY_DAYS } from "../constants.js"; +import { VALID_LOCALES } from "@yari-internal/constants"; +import { redirect } from "../utils.js"; + +// Note that the keys of "VALID_LOCALES" are lowercase locales. +const LOCALE_URI_WITHOUT_TRAILING_SLASH = new Set( + [...VALID_LOCALES.keys()].map((locale) => `/${locale}`) +); +const LOCALE_URI_WITH_TRAILING_SLASH = new Set( + [...VALID_LOCALES.keys()].map((locale) => `/${locale}/`) +); +// TODO: The code that uses LEGACY_URI_NEEDING_TRAILING_SLASH should be +// temporary. For example, when we have moved to the Yari-built +// account settings page, we should add fundamental redirects +// for "/{locale}/account/?" and "/account/?" that redirect to +// "/{locale}/settings" and "/settings" respectively. The other +// cases can be either redirected or deleted eventually as well. +// The goal is to eventually remove the code that uses +// LEGACY_URI_NEEDING_TRAILING_SLASH. +const LEGACY_URI_NEEDING_TRAILING_SLASH = new RegExp( + `^(?:${[...LOCALE_URI_WITHOUT_TRAILING_SLASH].join( + "|" + )})?/(?:account|contribute|maintenance-mode|payments)/?$` +); + +export function redirectTrailingSlash( + req: express.Request, + res: express.Response, + next: express.NextFunction +) { + const url = new URL(req.url, `${req.protocol}://${req.headers.host}`); + let requestURI = url.pathname; + const requestURILowerCase = requestURI.toLowerCase(); + const qs = url.search; + + // Handle cases related to the presence or absence of a trailing-slash. + if (LOCALE_URI_WITHOUT_TRAILING_SLASH.has(requestURILowerCase)) { + // Home page requests are the special case on MDN. They should + // always have a trailing slash. So a home page URL without a + // trailing slash should redirect to the same URL with a + // trailing slash. When the redirected home-page request is + // processed by this Lambda function, note that we'll remove + // the trailing slash before the request reaches S3 (see below). + return redirect(res, requestURI + "/" + qs, { + cacheControlSeconds: THIRTY_DAYS, + }); + } else if (LOCALE_URI_WITH_TRAILING_SLASH.has(requestURILowerCase)) { + // We've received a proper request for a locale's home page (i.e., + // it has a traling slash), but since that request will be served + // from S3, we need to strip the trailing slash before it reaches + // S3. This is required because we store the home pages in S3 as + // their path name itself, for example "en-us" for the English home + // page, not "en-us/index.html", which is what S3 would look for if + // we left the trailing slash. + requestURI = requestURI.slice(0, -1); + } else if ( + requestURI.endsWith("/") && + !LEGACY_URI_NEEDING_TRAILING_SLASH.test(requestURILowerCase) + ) { + // All other requests with a trailing slash should redirect to the + // same URL without the trailing slash. + return redirect(res, requestURI.slice(0, -1) + qs, { + cacheControlSeconds: THIRTY_DAYS, + }); + } + + next(); +} From d6644bf0fef95a6fdfb0c334a0387edf1ef42d8f Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 22 Mar 2023 23:26:52 +0100 Subject: [PATCH 100/343] chore(gcp/function): remove contentOriginRequest middleware --- gcp/function/src/app.ts | 5 +-- .../src/middlewares/content-origin-request.ts | 38 ------------------- 2 files changed, 1 insertion(+), 42 deletions(-) delete mode 100644 gcp/function/src/middlewares/content-origin-request.ts diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 0e8ca7db3df1..b3313fed3b4e 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -8,7 +8,6 @@ import { proxyRumba } from "./handlers/rumba.js"; import { plans } from "./handlers/plans.js"; import { proxyTelemetry } from "./handlers/telemetry.js"; import { pathnameLC } from "./middlewares/pathnameLC.js"; -import { contentOriginRequest } from "./middlewares/content-origin-request.js"; import { resolveIndexHTML } from "./middlewares/resolveIndexHTML.js"; import { redirectLeadingSlash } from "./middlewares/redirectLeadingSlash.js"; import { redirectMovedPages } from "./middlewares/redirectMovedPages.js"; @@ -34,18 +33,16 @@ mainRouter.get( redirectFundamental, redirectLocale, redirectTrailingSlash, - contentOriginRequest, redirectMovedPages, resolveIndexHTML, proxyContent ); -mainRouter.get("/[^/]+/search-index.json", contentOriginRequest, proxyContent); +mainRouter.get("/[^/]+/search-index.json", proxyContent); mainRouter.get( "*", redirectFundamental, redirectLocale, redirectTrailingSlash, - contentOriginRequest, resolveIndexHTML, proxyContent ); diff --git a/gcp/function/src/middlewares/content-origin-request.ts b/gcp/function/src/middlewares/content-origin-request.ts deleted file mode 100644 index 243ecf3e095b..000000000000 --- a/gcp/function/src/middlewares/content-origin-request.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type express from "express"; - -export function contentOriginRequest( - req: express.Request, - res: express.Response, - next: express.NextFunction -) { - function redirect( - location: string, - { status = 302, cacheControlSeconds = 0 } = {} - ) { - let cacheControlValue; - if (cacheControlSeconds) { - cacheControlValue = `max-age=${cacheControlSeconds},public`; - } else { - cacheControlValue = "no-store"; - } - - res.set("Cache-Control", cacheControlValue); - - // We need to URL encode the pathname, but leave the query string as is. - // Suppose the old URL was `/search?q=text%2Dshadow` and all we need to do - // is to inject the locale to that URL, we should not URL encode the whole - // new URL otherwise you'd end up with `/en-US/search?q=text%252Dshadow` - // since the already encoded `%2D` would become `%252D` which is wrong and - // different. - const [pathname, querystring] = location.split("?", 2); - let newLocation = encodeURI(pathname || ""); - if (querystring) { - newLocation += `?${querystring}`; - } - - res.redirect(status, newLocation); - next(); - } - - next(); -} From cd0f5e3a2f4cbe60d51b793f43e5d8ee472ca389 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 22 Mar 2023 23:56:46 +0100 Subject: [PATCH 101/343] refactor(gcp/function): extract plans to TS --- gcp/function/src/handlers/plans.ts | 51 +- gcp/function/src/plans/index.ts | 37 + .../plans-prod-lookup.json => plans/prod.ts} | 3828 ++++++++-------- .../stage.ts} | 3830 +++++++++-------- 4 files changed, 3875 insertions(+), 3871 deletions(-) create mode 100644 gcp/function/src/plans/index.ts rename gcp/function/src/{handlers/plans-prod-lookup.json => plans/prod.ts} (62%) rename gcp/function/src/{handlers/plans-stage-lookup.json => plans/stage.ts} (64%) diff --git a/gcp/function/src/handlers/plans.ts b/gcp/function/src/handlers/plans.ts index cc02f5658321..7bfc417521a3 100644 --- a/gcp/function/src/handlers/plans.ts +++ b/gcp/function/src/handlers/plans.ts @@ -1,63 +1,22 @@ import type express from "express"; -import { createRequire } from "node:module"; import acceptLanguageParser from "accept-language-parser"; import { ORIGIN_MAIN } from "../env.js"; import { getRequestCountry } from "../utils.js"; - -const require = createRequire(import.meta.url); - -const stageLookup = require("./plans-stage-lookup.json") satisfies LookupData; -const prodLookup = require("./plans-prod-lookup.json") satisfies LookupData; - -type CountryCode = string; -type LanguageCode = string; -type CurrencyCode = string; -type ProductId = string; -type PriceId = string; - -interface Plan { - unit_amount: number; - currency: CurrencyCode; - product: ProductId; - recurring: { - interval: "month" | "year"; - }; - nickname: string; - lookup_key: string; - metadata: { [key: string]: string }; - price_id: PriceId; -} -interface LookupData { - countryToCurrency: { - [countryCode: CountryCode]: { - currency: CurrencyCode; - supportedLanguages: { - [languageCode: LanguageCode]: null | { - [key: string]: string; - }; - }; - defaultLanguage: LanguageCode; - }; - }; - langCurrencyToPlans: { - [langCurrencyCode: string]: { - [name: string]: Plan; - }; - }; -} +import stageLookup from "../plans/stage.js"; +import prodLookup from "../plans/prod.js"; interface PlanResult { [name: string]: { monthlyPriceInCents: number; id: string }; } interface Result { - country: CountryCode; - currency: CurrencyCode; + country: string; + currency: string; plans: PlanResult; } export function plans(req: express.Request, res: express.Response) { - const lookupData: LookupData = + const lookupData = ORIGIN_MAIN === "developer.mozilla.org" ? prodLookup : stageLookup; const localeHeader = req.headers["accept-language"]; diff --git a/gcp/function/src/plans/index.ts b/gcp/function/src/plans/index.ts new file mode 100644 index 000000000000..b9ce12359206 --- /dev/null +++ b/gcp/function/src/plans/index.ts @@ -0,0 +1,37 @@ +type CountryCode = string; +type LanguageCode = string; +type CurrencyCode = string; +type ProductId = string; +type PriceId = string; + +export interface Plan { + unit_amount: number; + currency: CurrencyCode; + product: ProductId; + recurring: { + interval: "month" | "year"; + }; + nickname: string; + lookup_key: string; + metadata: { [key: string]: string }; + price_id: PriceId; +} + +export interface LookupData { + countryToCurrency: { + [countryCode: CountryCode]: { + currency: CurrencyCode; + supportedLanguages: { + [languageCode: LanguageCode]: null | { + [key: string]: string; + }; + }; + defaultLanguage: LanguageCode; + }; + }; + langCurrencyToPlans: { + [langCurrencyCode: string]: { + [name: string]: Plan; + }; + }; +} diff --git a/gcp/function/src/handlers/plans-prod-lookup.json b/gcp/function/src/plans/prod.ts similarity index 62% rename from gcp/function/src/handlers/plans-prod-lookup.json rename to gcp/function/src/plans/prod.ts index c89930fdbfe9..9e8698a83f65 100644 --- a/gcp/function/src/handlers/plans-prod-lookup.json +++ b/gcp/function/src/plans/prod.ts @@ -1,3705 +1,3709 @@ -{ - "countryToCurrency": { - "AS": { - "currency": "usd", - "supportedLanguages": { - "en": null - }, - "defaultLanguage": "en" +import type { LookupData } from "./index.js"; + +const plans: LookupData = { + countryToCurrency: { + AS: { + currency: "usd", + supportedLanguages: { + en: null, + }, + defaultLanguage: "en", }, - "CA": { - "currency": "usd", - "supportedLanguages": { - "en": null + CA: { + currency: "usd", + supportedLanguages: { + en: null, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "GB": { - "currency": "usd", - "supportedLanguages": { - "en": null + GB: { + currency: "usd", + supportedLanguages: { + en: null, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "GU": { - "currency": "usd", - "supportedLanguages": { - "en": null + GU: { + currency: "usd", + supportedLanguages: { + en: null, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "MP": { - "currency": "usd", - "supportedLanguages": { - "en": null + MP: { + currency: "usd", + supportedLanguages: { + en: null, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "MY": { - "currency": "usd", - "supportedLanguages": { - "en": null + MY: { + currency: "usd", + supportedLanguages: { + en: null, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "NZ": { - "currency": "usd", - "supportedLanguages": { - "en": null + NZ: { + currency: "usd", + supportedLanguages: { + en: null, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "PR": { - "currency": "usd", - "supportedLanguages": { - "en": null + PR: { + currency: "usd", + supportedLanguages: { + en: null, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "SG": { - "currency": "usd", - "supportedLanguages": { - "en": null + SG: { + currency: "usd", + supportedLanguages: { + en: null, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "US": { - "currency": "usd", - "supportedLanguages": { - "en": null + US: { + currency: "usd", + supportedLanguages: { + en: null, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "VI": { - "currency": "usd", - "supportedLanguages": { - "en": null + VI: { + currency: "usd", + supportedLanguages: { + en: null, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "AT": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + AT: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "de" + defaultLanguage: "de", }, - "BE": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + BE: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "nl" + defaultLanguage: "nl", }, - "CY": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + CY: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "el" + defaultLanguage: "el", }, - "DE": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + DE: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "de" + defaultLanguage: "de", }, - "EE": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + EE: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "et" + defaultLanguage: "et", }, - "ES": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + ES: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "es" + defaultLanguage: "es", }, - "FI": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + FI: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "FR": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + FR: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "fr" + defaultLanguage: "fr", }, - "GR": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + GR: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "el" + defaultLanguage: "el", }, - "IE": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + IE: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "IT": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + IT: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "it" + defaultLanguage: "it", }, - "LT": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + LT: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "lt" + defaultLanguage: "lt", }, - "LU": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + LU: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "fr" + defaultLanguage: "fr", }, - "LV": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + LV: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "lv" + defaultLanguage: "lv", }, - "MT": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + MT: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "NL": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + NL: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "nl" + defaultLanguage: "nl", }, - "SE": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + SE: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "SK": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + SK: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "sk" + defaultLanguage: "sk", }, - "SI": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + SI: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "sl" + defaultLanguage: "sl", }, - "PT": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + PT: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "pt" + defaultLanguage: "pt", }, - "CH": { - "currency": "chf", - "supportedLanguages": { - "en": null, - "de": { + CH: { + currency: "chf", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" - } + "product:details:3": "MDN offline", + }, }, - "defaultLanguage": "de" - } + defaultLanguage: "de", + }, }, - "langCurrencyToPlans": { + langCurrencyToPlans: { "usd-en": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "usd", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - USD EN", - "lookup_key": "usd-en-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "usd", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - USD EN", + lookup_key: "usd-en-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:720bc80adfa6988d": "mdn_plus_5m", "product:successActionButtonLabel": "Continue to MDN Plus", "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KeG02JNcmPzuWtR1oBrw8o6" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "usd", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - USD EN", - "lookup_key": "usd-en-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN offline", + }, + price_id: "price_1KeG02JNcmPzuWtR1oBrw8o6", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "usd", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - USD EN", + lookup_key: "usd-en-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:720bc80adfa6988d": "mdn_plus_5y", "product:successActionButtonLabel": "Continue to MDN Plus", "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KeG02JNcmPzuWtRslZijhQu" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "usd", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - USD EN", - "lookup_key": "usd-en-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN offline", + }, + price_id: "price_1KeG02JNcmPzuWtRslZijhQu", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "usd", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - USD EN", + lookup_key: "usd-en-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:720bc80adfa6988d": "mdn_plus_10m", "product:successActionButtonLabel": "Continue to MDN Plus", "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KeG02JNcmPzuWtRuAnIgNHh" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "usd", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - USD EN", - "lookup_key": "usd-en-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN offline", + }, + price_id: "price_1KeG02JNcmPzuWtRuAnIgNHh", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "usd", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - USD EN", + lookup_key: "usd-en-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:720bc80adfa6988d": "mdn_plus_10y", "product:successActionButtonLabel": "Continue to MDN Plus", "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "price_id": "price_1KeG02JNcmPzuWtRlrSiLTI6" - } + price_id: "price_1KeG02JNcmPzuWtRlrSiLTI6", + }, }, "eur-en": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR EN", - "lookup_key": "eur-en-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR EN", + lookup_key: "eur-en-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:720bc80adfa6988d": "mdn_plus_5m", "product:successActionButtonLabel": "Continue to MDN Plus", "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqeX9JNcmPzuWtRJNelT86c" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR EN", - "lookup_key": "eur-en-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqeX9JNcmPzuWtRJNelT86c", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR EN", + lookup_key: "eur-en-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:720bc80adfa6988d": "mdn_plus_5y", "product:successActionButtonLabel": "Continue to MDN Plus", "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqeX9JNcmPzuWtR4dOStCqA" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR EN", - "lookup_key": "eur-en-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqeX9JNcmPzuWtR4dOStCqA", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR EN", + lookup_key: "eur-en-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:720bc80adfa6988d": "mdn_plus_10m", "product:successActionButtonLabel": "Continue to MDN Plus", "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqeXAJNcmPzuWtRjvfOVIUP" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR EN", - "lookup_key": "eur-en-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqeXAJNcmPzuWtRjvfOVIUP", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR EN", + lookup_key: "eur-en-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:720bc80adfa6988d": "mdn_plus_10y", "product:successActionButtonLabel": "Continue to MDN Plus", "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "price_id": "price_1KqeXAJNcmPzuWtR5hwpwsUr" - } + price_id: "price_1KqeXAJNcmPzuWtR5hwpwsUr", + }, }, "eur-de": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR DE", - "lookup_key": "eur-de-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR DE", + lookup_key: "eur-de-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:720bc80adfa6988d": "mdn_plus_5m", "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqeXBJNcmPzuWtR7opydXog" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR DE", - "lookup_key": "eur-de-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqeXBJNcmPzuWtR7opydXog", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR DE", + lookup_key: "eur-de-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:720bc80adfa6988d": "mdn_plus_5y", "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqeXBJNcmPzuWtRBSY6DGlv" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR DE", - "lookup_key": "eur-de-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqeXBJNcmPzuWtRBSY6DGlv", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR DE", + lookup_key: "eur-de-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:720bc80adfa6988d": "mdn_plus_10m", "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqeXBJNcmPzuWtRNWzQ6IX4" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR DE", - "lookup_key": "eur-de-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqeXBJNcmPzuWtRNWzQ6IX4", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR DE", + lookup_key: "eur-de-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:720bc80adfa6988d": "mdn_plus_10y", "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "price_id": "price_1KqeXCJNcmPzuWtR5fl7C1tx" - } + price_id: "price_1KqeXCJNcmPzuWtR5fl7C1tx", + }, }, "eur-el": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR EL", - "lookup_key": "eur-el-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR EL", + lookup_key: "eur-el-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:720bc80adfa6988d": "mdn_plus_5m", "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" - }, - "price_id": "price_1L5915JNcmPzuWtRMXzNAj50" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR EL", - "lookup_key": "eur-el-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "Συνέχεια σε MDN Plus", + }, + price_id: "price_1L5915JNcmPzuWtRMXzNAj50", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR EL", + lookup_key: "eur-el-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:720bc80adfa6988d": "mdn_plus_5y", "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" - }, - "price_id": "price_1L5916JNcmPzuWtRdYKhF10o" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR EL", - "lookup_key": "eur-el-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "Συνέχεια σε MDN Plus", + }, + price_id: "price_1L5916JNcmPzuWtRdYKhF10o", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR EL", + lookup_key: "eur-el-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:720bc80adfa6988d": "mdn_plus_10m", "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" - }, - "price_id": "price_1L5916JNcmPzuWtRkCVnoV9Y" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR EL", - "lookup_key": "eur-el-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "Συνέχεια σε MDN Plus", + }, + price_id: "price_1L5916JNcmPzuWtRkCVnoV9Y", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR EL", + lookup_key: "eur-el-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:720bc80adfa6988d": "mdn_plus_10y", "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "price_id": "price_1L5917JNcmPzuWtRGcDOMndA" - } + price_id: "price_1L5917JNcmPzuWtRGcDOMndA", + }, }, "eur-es": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR ES", - "lookup_key": "eur-es-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR ES", + lookup_key: "eur-es-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:720bc80adfa6988d": "mdn_plus_5m", "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" - }, - "price_id": "price_1KqeXCJNcmPzuWtR05tPDWqA" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR ES", - "lookup_key": "eur-es-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN disponible sin conexión", + }, + price_id: "price_1KqeXCJNcmPzuWtR05tPDWqA", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR ES", + lookup_key: "eur-es-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:720bc80adfa6988d": "mdn_plus_5y", "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" - }, - "price_id": "price_1KqeXCJNcmPzuWtR5yy0wlfO" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR ES", - "lookup_key": "eur-es-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN disponible sin conexión", + }, + price_id: "price_1KqeXCJNcmPzuWtR5yy0wlfO", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR ES", + lookup_key: "eur-es-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:720bc80adfa6988d": "mdn_plus_10m", "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" - }, - "price_id": "price_1KqeXDJNcmPzuWtRJAstOkqA" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR ES", - "lookup_key": "eur-es-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN disponible sin conexión", + }, + price_id: "price_1KqeXDJNcmPzuWtRJAstOkqA", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR ES", + lookup_key: "eur-es-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:720bc80adfa6988d": "mdn_plus_10y", "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "price_id": "price_1KqeXDJNcmPzuWtR8hPdxJiS" - } + price_id: "price_1KqeXDJNcmPzuWtR8hPdxJiS", + }, }, "eur-et": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR ET", - "lookup_key": "eur-et-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR ET", + lookup_key: "eur-et-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:720bc80adfa6988d": "mdn_plus_5m", "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" - }, - "price_id": "price_1L5917JNcmPzuWtRXU88NcRx" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR ET", - "lookup_key": "eur-et-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN offline", + }, + price_id: "price_1L5917JNcmPzuWtRXU88NcRx", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR ET", + lookup_key: "eur-et-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:720bc80adfa6988d": "mdn_plus_5y", "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" - }, - "price_id": "price_1L5918JNcmPzuWtRYHCMLCWR" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR ET", - "lookup_key": "eur-et-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN offline", + }, + price_id: "price_1L5918JNcmPzuWtRYHCMLCWR", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR ET", + lookup_key: "eur-et-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:720bc80adfa6988d": "mdn_plus_10m", "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" - }, - "price_id": "price_1L5918JNcmPzuWtRkS2tNVZC" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR ET", - "lookup_key": "eur-et-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN offline", + }, + price_id: "price_1L5918JNcmPzuWtRkS2tNVZC", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR ET", + lookup_key: "eur-et-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:720bc80adfa6988d": "mdn_plus_10y", "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "price_id": "price_1L5918JNcmPzuWtRl47Ws8m0" - } + price_id: "price_1L5918JNcmPzuWtRl47Ws8m0", + }, }, "eur-fr": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR FR", - "lookup_key": "eur-fr-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR FR", + lookup_key: "eur-fr-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:720bc80adfa6988d": "mdn_plus_5m", "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" - }, - "price_id": "price_1KqeXDJNcmPzuWtRoL9NNeK4" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR FR", - "lookup_key": "eur-fr-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN hors ligne", + }, + price_id: "price_1KqeXDJNcmPzuWtRoL9NNeK4", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR FR", + lookup_key: "eur-fr-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:720bc80adfa6988d": "mdn_plus_5y", "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" - }, - "price_id": "price_1KqeXEJNcmPzuWtRTw1w8bX5" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR FR", - "lookup_key": "eur-fr-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN hors ligne", + }, + price_id: "price_1KqeXEJNcmPzuWtRTw1w8bX5", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR FR", + lookup_key: "eur-fr-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:720bc80adfa6988d": "mdn_plus_10m", "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" - }, - "price_id": "price_1KqeXEJNcmPzuWtR3WZLuJ6K" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR FR", - "lookup_key": "eur-fr-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN hors ligne", + }, + price_id: "price_1KqeXEJNcmPzuWtR3WZLuJ6K", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR FR", + lookup_key: "eur-fr-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:720bc80adfa6988d": "mdn_plus_10y", "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "price_id": "price_1KqeXEJNcmPzuWtR1kOfChz0" - } + price_id: "price_1KqeXEJNcmPzuWtR1kOfChz0", + }, }, "eur-it": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR IT", - "lookup_key": "eur-it-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR IT", + lookup_key: "eur-it-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:720bc80adfa6988d": "mdn_plus_5m", "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqeXFJNcmPzuWtRUBiVlTVX" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR IT", - "lookup_key": "eur-it-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqeXFJNcmPzuWtRUBiVlTVX", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR IT", + lookup_key: "eur-it-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:720bc80adfa6988d": "mdn_plus_5y", "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqeXFJNcmPzuWtRjdDWnMU6" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR IT", - "lookup_key": "eur-it-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqeXFJNcmPzuWtRjdDWnMU6", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR IT", + lookup_key: "eur-it-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:720bc80adfa6988d": "mdn_plus_10m", "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqeXFJNcmPzuWtR2UJ1TVSG" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR IT", - "lookup_key": "eur-it-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqeXFJNcmPzuWtR2UJ1TVSG", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR IT", + lookup_key: "eur-it-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:720bc80adfa6988d": "mdn_plus_10y", "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "price_id": "price_1KqeXGJNcmPzuWtR7cw3rh90" - } + price_id: "price_1KqeXGJNcmPzuWtR7cw3rh90", + }, }, "eur-lt": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR LT", - "lookup_key": "eur-lt-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR LT", + lookup_key: "eur-lt-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:720bc80adfa6988d": "mdn_plus_5m", "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" - }, - "price_id": "price_1L5919JNcmPzuWtRvhirUtKK" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR LT", - "lookup_key": "eur-lt-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN neprisijungus", + }, + price_id: "price_1L5919JNcmPzuWtRvhirUtKK", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR LT", + lookup_key: "eur-lt-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:720bc80adfa6988d": "mdn_plus_5y", "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" - }, - "price_id": "price_1L5919JNcmPzuWtRNlOkp6pJ" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR LT", - "lookup_key": "eur-lt-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN neprisijungus", + }, + price_id: "price_1L5919JNcmPzuWtRNlOkp6pJ", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR LT", + lookup_key: "eur-lt-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:720bc80adfa6988d": "mdn_plus_10m", "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" - }, - "price_id": "price_1L591AJNcmPzuWtR0YIkMvZC" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR LT", - "lookup_key": "eur-lt-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN neprisijungus", + }, + price_id: "price_1L591AJNcmPzuWtR0YIkMvZC", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR LT", + lookup_key: "eur-lt-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:720bc80adfa6988d": "mdn_plus_10y", "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "price_id": "price_1L591AJNcmPzuWtRK2eZPpI9" - } + price_id: "price_1L591AJNcmPzuWtRK2eZPpI9", + }, }, "eur-lv": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR LV", - "lookup_key": "eur-lv-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR LV", + lookup_key: "eur-lv-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:720bc80adfa6988d": "mdn_plus_5m", "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" - }, - "price_id": "price_1L591AJNcmPzuWtRNotb24QG" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR LV", - "lookup_key": "eur-lv-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN bezsaistē", + }, + price_id: "price_1L591AJNcmPzuWtRNotb24QG", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR LV", + lookup_key: "eur-lv-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:720bc80adfa6988d": "mdn_plus_5y", "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" - }, - "price_id": "price_1L591BJNcmPzuWtRS32AB1gs" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR LV", - "lookup_key": "eur-lv-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN bezsaistē", + }, + price_id: "price_1L591BJNcmPzuWtRS32AB1gs", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR LV", + lookup_key: "eur-lv-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:720bc80adfa6988d": "mdn_plus_10m", "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" - }, - "price_id": "price_1L591BJNcmPzuWtRk7skMZRp" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR LV", - "lookup_key": "eur-lv-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN bezsaistē", + }, + price_id: "price_1L591BJNcmPzuWtRk7skMZRp", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR LV", + lookup_key: "eur-lv-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:720bc80adfa6988d": "mdn_plus_10y", "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "price_id": "price_1L591CJNcmPzuWtRUUa1ybJs" - } + price_id: "price_1L591CJNcmPzuWtRUUa1ybJs", + }, }, "eur-nl": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR NL", - "lookup_key": "eur-nl-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR NL", + lookup_key: "eur-nl-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:720bc80adfa6988d": "mdn_plus_5m", "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqeXGJNcmPzuWtRcjH3vbwC" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR NL", - "lookup_key": "eur-nl-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqeXGJNcmPzuWtRcjH3vbwC", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR NL", + lookup_key: "eur-nl-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:720bc80adfa6988d": "mdn_plus_5y", "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqeXGJNcmPzuWtRbycawcNr" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR NL", - "lookup_key": "eur-nl-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqeXGJNcmPzuWtRbycawcNr", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR NL", + lookup_key: "eur-nl-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:720bc80adfa6988d": "mdn_plus_10m", "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqeXHJNcmPzuWtRxV9W8tIQ" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR NL", - "lookup_key": "eur-nl-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqeXHJNcmPzuWtRxV9W8tIQ", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR NL", + lookup_key: "eur-nl-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:720bc80adfa6988d": "mdn_plus_10y", "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "price_id": "price_1KqeXHJNcmPzuWtR9IvbHgFI" - } + price_id: "price_1KqeXHJNcmPzuWtR9IvbHgFI", + }, }, "eur-pt": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR PT", - "lookup_key": "eur-pt-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR PT", + lookup_key: "eur-pt-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:720bc80adfa6988d": "mdn_plus_5m", "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" - }, - "price_id": "price_1L591CJNcmPzuWtRhzqtQqRW" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR PT", - "lookup_key": "eur-pt-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN Offline", + }, + price_id: "price_1L591CJNcmPzuWtRhzqtQqRW", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR PT", + lookup_key: "eur-pt-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:720bc80adfa6988d": "mdn_plus_5y", "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" - }, - "price_id": "price_1L591CJNcmPzuWtRkmi7uEDk" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR PT", - "lookup_key": "eur-pt-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN Offline", + }, + price_id: "price_1L591CJNcmPzuWtRkmi7uEDk", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR PT", + lookup_key: "eur-pt-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:720bc80adfa6988d": "mdn_plus_10m", "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" - }, - "price_id": "price_1L591DJNcmPzuWtRQH8Mybz5" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR PT", - "lookup_key": "eur-pt-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN Offline", + }, + price_id: "price_1L591DJNcmPzuWtRQH8Mybz5", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR PT", + lookup_key: "eur-pt-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:720bc80adfa6988d": "mdn_plus_10y", "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "price_id": "price_1L591DJNcmPzuWtRFZNp49Py" - } + price_id: "price_1L591DJNcmPzuWtRFZNp49Py", + }, }, "eur-sk": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR SK", - "lookup_key": "eur-sk-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR SK", + lookup_key: "eur-sk-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:720bc80adfa6988d": "mdn_plus_5m", "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" - }, - "price_id": "price_1L591DJNcmPzuWtRdBWgbH2y" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR SK", - "lookup_key": "eur-sk-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN offline", + }, + price_id: "price_1L591DJNcmPzuWtRdBWgbH2y", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR SK", + lookup_key: "eur-sk-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:720bc80adfa6988d": "mdn_plus_5y", "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" - }, - "price_id": "price_1L591EJNcmPzuWtRL5J0OPcy" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR SK", - "lookup_key": "eur-sk-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN offline", + }, + price_id: "price_1L591EJNcmPzuWtRL5J0OPcy", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR SK", + lookup_key: "eur-sk-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:720bc80adfa6988d": "mdn_plus_10m", "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" - }, - "price_id": "price_1L591EJNcmPzuWtRKLalyUPd" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR SK", - "lookup_key": "eur-sk-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN offline", + }, + price_id: "price_1L591EJNcmPzuWtRKLalyUPd", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR SK", + lookup_key: "eur-sk-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:720bc80adfa6988d": "mdn_plus_10y", "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "price_id": "price_1L591EJNcmPzuWtRwJobTR51" - } + price_id: "price_1L591EJNcmPzuWtRwJobTR51", + }, }, "eur-sl": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR SL", - "lookup_key": "eur-sl-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR SL", + lookup_key: "eur-sl-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:720bc80adfa6988d": "mdn_plus_5m", "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" - }, - "price_id": "price_1L591FJNcmPzuWtRIuDMHIsn" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR SL", - "lookup_key": "eur-sl-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN brez povezave", + }, + price_id: "price_1L591FJNcmPzuWtRIuDMHIsn", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR SL", + lookup_key: "eur-sl-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:720bc80adfa6988d": "mdn_plus_5y", "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" - }, - "price_id": "price_1L591FJNcmPzuWtRAa0tq6bw" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR SL", - "lookup_key": "eur-sl-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN brez povezave", + }, + price_id: "price_1L591FJNcmPzuWtRAa0tq6bw", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR SL", + lookup_key: "eur-sl-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:720bc80adfa6988d": "mdn_plus_10m", "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" - }, - "price_id": "price_1L591FJNcmPzuWtRduAZLny6" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR SL", - "lookup_key": "eur-sl-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN brez povezave", + }, + price_id: "price_1L591FJNcmPzuWtRduAZLny6", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR SL", + lookup_key: "eur-sl-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:720bc80adfa6988d": "mdn_plus_10y", "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "price_id": "price_1L591GJNcmPzuWtRilCry5VT" - } + price_id: "price_1L591GJNcmPzuWtRilCry5VT", + }, }, "eur-tr": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR TR", - "lookup_key": "eur-tr-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR TR", + lookup_key: "eur-tr-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:720bc80adfa6988d": "mdn_plus_5m", "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - }, - "price_id": "price_1L591GJNcmPzuWtRLDAVfALF" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR TR", - "lookup_key": "eur-tr-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN çevrim dışı", + }, + price_id: "price_1L591GJNcmPzuWtRLDAVfALF", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR TR", + lookup_key: "eur-tr-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:720bc80adfa6988d": "mdn_plus_5y", "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - }, - "price_id": "price_1L591GJNcmPzuWtRU3dE0gMp" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR TR", - "lookup_key": "eur-tr-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN çevrim dışı", + }, + price_id: "price_1L591GJNcmPzuWtRU3dE0gMp", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR TR", + lookup_key: "eur-tr-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:720bc80adfa6988d": "mdn_plus_10m", "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - }, - "price_id": "price_1L591HJNcmPzuWtRCh2COINB" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR TR", - "lookup_key": "eur-tr-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN çevrim dışı", + }, + price_id: "price_1L591HJNcmPzuWtRCh2COINB", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR TR", + lookup_key: "eur-tr-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:720bc80adfa6988d": "mdn_plus_10y", "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" + "product:details:3": "MDN çevrim dışı", }, - "price_id": "price_1L591HJNcmPzuWtR6xBbf4ah" - } + price_id: "price_1L591HJNcmPzuWtR6xBbf4ah", + }, }, "chf-en": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "chf", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - CHF EN", - "lookup_key": "chf-en-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "chf", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - CHF EN", + lookup_key: "chf-en-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:720bc80adfa6988d": "mdn_plus_5m", "product:successActionButtonLabel": "Continue to MDN Plus", "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqeXHJNcmPzuWtR4itTRF7Y" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "chf", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - CHF EN", - "lookup_key": "chf-en-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqeXHJNcmPzuWtR4itTRF7Y", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "chf", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - CHF EN", + lookup_key: "chf-en-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:720bc80adfa6988d": "mdn_plus_5y", "product:successActionButtonLabel": "Continue to MDN Plus", "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqeXIJNcmPzuWtRZJlZhXpW" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "chf", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - CHF EN", - "lookup_key": "chf-en-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqeXIJNcmPzuWtRZJlZhXpW", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "chf", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - CHF EN", + lookup_key: "chf-en-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:720bc80adfa6988d": "mdn_plus_10m", "product:successActionButtonLabel": "Continue to MDN Plus", "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqeXIJNcmPzuWtRKwoSofy2" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "chf", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - CHF EN", - "lookup_key": "chf-en-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqeXIJNcmPzuWtRKwoSofy2", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "chf", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - CHF EN", + lookup_key: "chf-en-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:720bc80adfa6988d": "mdn_plus_10y", "product:successActionButtonLabel": "Continue to MDN Plus", "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "price_id": "price_1KqeXJJNcmPzuWtRDSDHSlAl" - } + price_id: "price_1KqeXJJNcmPzuWtRDSDHSlAl", + }, }, "chf-de": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "chf", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - CHF DE", - "lookup_key": "chf-de-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "chf", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - CHF DE", + lookup_key: "chf-de-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:720bc80adfa6988d": "mdn_plus_5m", "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqeXJJNcmPzuWtRDFlszhVO" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "chf", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - CHF DE", - "lookup_key": "chf-de-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqeXJJNcmPzuWtRDFlszhVO", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "chf", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - CHF DE", + lookup_key: "chf-de-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:720bc80adfa6988d": "mdn_plus_5y", "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqeXKJNcmPzuWtREcYZdrje" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "chf", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - CHF DE", - "lookup_key": "chf-de-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqeXKJNcmPzuWtREcYZdrje", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "chf", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - CHF DE", + lookup_key: "chf-de-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:720bc80adfa6988d": "mdn_plus_10m", "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqeXKJNcmPzuWtRExRB58k0" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "chf", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - CHF DE", - "lookup_key": "chf-de-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqeXKJNcmPzuWtRExRB58k0", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "chf", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - CHF DE", + lookup_key: "chf-de-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:720bc80adfa6988d": "mdn_plus_10y", "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "price_id": "price_1KqeXLJNcmPzuWtRtypFouG4" - } + price_id: "price_1KqeXLJNcmPzuWtRtypFouG4", + }, }, "chf-fr": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "chf", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - CHF FR", - "lookup_key": "chf-fr-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "chf", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - CHF FR", + lookup_key: "chf-fr-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:720bc80adfa6988d": "mdn_plus_5m", "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" - }, - "price_id": "price_1KqeXLJNcmPzuWtRyS2uTKyE" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "chf", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - CHF FR", - "lookup_key": "chf-fr-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN hors ligne", + }, + price_id: "price_1KqeXLJNcmPzuWtRyS2uTKyE", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "chf", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - CHF FR", + lookup_key: "chf-fr-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:720bc80adfa6988d": "mdn_plus_5y", "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" - }, - "price_id": "price_1KqeXLJNcmPzuWtRbH6wD6sm" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "chf", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - CHF FR", - "lookup_key": "chf-fr-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN hors ligne", + }, + price_id: "price_1KqeXLJNcmPzuWtRbH6wD6sm", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "chf", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - CHF FR", + lookup_key: "chf-fr-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:720bc80adfa6988d": "mdn_plus_10m", "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" - }, - "price_id": "price_1KqeXMJNcmPzuWtR46VuMNtb" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "chf", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - CHF FR", - "lookup_key": "chf-fr-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN hors ligne", + }, + price_id: "price_1KqeXMJNcmPzuWtR46VuMNtb", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "chf", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - CHF FR", + lookup_key: "chf-fr-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:720bc80adfa6988d": "mdn_plus_10y", "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "price_id": "price_1KqeXMJNcmPzuWtR3bNmxM4C" - } + price_id: "price_1KqeXMJNcmPzuWtR3bNmxM4C", + }, }, "chf-it": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "chf", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - CHF IT", - "lookup_key": "chf-it-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "chf", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - CHF IT", + lookup_key: "chf-it-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:720bc80adfa6988d": "mdn_plus_5m", "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqeXMJNcmPzuWtRC7IJcwUD" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "chf", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - CHF IT", - "lookup_key": "chf-it-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqeXMJNcmPzuWtRC7IJcwUD", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "chf", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - CHF IT", + lookup_key: "chf-it-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:720bc80adfa6988d": "mdn_plus_5y", "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqeXNJNcmPzuWtRSCJ2GbVn" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "chf", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - CHF IT", - "lookup_key": "chf-it-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqeXNJNcmPzuWtRSCJ2GbVn", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "chf", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - CHF IT", + lookup_key: "chf-it-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:720bc80adfa6988d": "mdn_plus_10m", "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqeXNJNcmPzuWtRRb5z07dQ" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "chf", - "product": "prod_LKvr8fYGbBxcaZ", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - CHF IT", - "lookup_key": "chf-it-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqeXNJNcmPzuWtRRb5z07dQ", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "chf", + product: "prod_LKvr8fYGbBxcaZ", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - CHF IT", + lookup_key: "chf-it-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:720bc80adfa6988d": "mdn_plus_10y", "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "price_id": "price_1KqeXOJNcmPzuWtRMBae7104" - } - } - } -} + price_id: "price_1KqeXOJNcmPzuWtRMBae7104", + }, + }, + }, +}; + +export default plans; diff --git a/gcp/function/src/handlers/plans-stage-lookup.json b/gcp/function/src/plans/stage.ts similarity index 64% rename from gcp/function/src/handlers/plans-stage-lookup.json rename to gcp/function/src/plans/stage.ts index 910518716518..16f030e04e63 100644 --- a/gcp/function/src/handlers/plans-stage-lookup.json +++ b/gcp/function/src/plans/stage.ts @@ -1,2085 +1,2087 @@ -{ - "countryToCurrency": { - "AS": { - "currency": "usd", - "supportedLanguages": { - "en": null - }, - "defaultLanguage": "en" +import type { LookupData } from "./index.js"; + +const plans: LookupData = { + countryToCurrency: { + AS: { + currency: "usd", + supportedLanguages: { + en: null, + }, + defaultLanguage: "en", }, - "CA": { - "currency": "usd", - "supportedLanguages": { - "en": null + CA: { + currency: "usd", + supportedLanguages: { + en: null, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "GB": { - "currency": "usd", - "supportedLanguages": { - "en": null + GB: { + currency: "usd", + supportedLanguages: { + en: null, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "GU": { - "currency": "usd", - "supportedLanguages": { - "en": null + GU: { + currency: "usd", + supportedLanguages: { + en: null, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "MP": { - "currency": "usd", - "supportedLanguages": { - "en": null + MP: { + currency: "usd", + supportedLanguages: { + en: null, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "MY": { - "currency": "usd", - "supportedLanguages": { - "en": null + MY: { + currency: "usd", + supportedLanguages: { + en: null, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "NZ": { - "currency": "usd", - "supportedLanguages": { - "en": null + NZ: { + currency: "usd", + supportedLanguages: { + en: null, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "PR": { - "currency": "usd", - "supportedLanguages": { - "en": null + PR: { + currency: "usd", + supportedLanguages: { + en: null, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "SG": { - "currency": "usd", - "supportedLanguages": { - "en": null + SG: { + currency: "usd", + supportedLanguages: { + en: null, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "US": { - "currency": "usd", - "supportedLanguages": { - "en": null + US: { + currency: "usd", + supportedLanguages: { + en: null, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "VI": { - "currency": "usd", - "supportedLanguages": { - "en": null + VI: { + currency: "usd", + supportedLanguages: { + en: null, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "AT": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + AT: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "de" + defaultLanguage: "de", }, - "BE": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + BE: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "nl" + defaultLanguage: "nl", }, - "CY": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + CY: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "el" + defaultLanguage: "el", }, - "DE": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + DE: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "de" + defaultLanguage: "de", }, - "EE": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + EE: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "et" + defaultLanguage: "et", }, - "ES": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + ES: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "es" + defaultLanguage: "es", }, - "FI": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + FI: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "FR": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + FR: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "fr" + defaultLanguage: "fr", }, - "GR": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + GR: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "el" + defaultLanguage: "el", }, - "IE": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + IE: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "IT": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + IT: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "it" + defaultLanguage: "it", }, - "LT": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + LT: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "lt" + defaultLanguage: "lt", }, - "LU": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + LU: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "fr" + defaultLanguage: "fr", }, - "LV": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + LV: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "lv" + defaultLanguage: "lv", }, - "MT": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + MT: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "NL": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + NL: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "nl" + defaultLanguage: "nl", }, - "SE": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + SE: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "en" + defaultLanguage: "en", }, - "SK": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + SK: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "sk" + defaultLanguage: "sk", }, - "SI": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + SI: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "sl" + defaultLanguage: "sl", }, - "PT": { - "currency": "eur", - "supportedLanguages": { - "en": null, - "de": { + PT: { + currency: "eur", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "el": { + el: { "product:successActionButtonLabel": "Μια Υπηρεσία MDN Premium", "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "es": { + es: { "product:successActionButtonLabel": "Continuar a MDN Plus", "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "et": { + et: { "product:successActionButtonLabel": "MDN Premium Service", "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "lt": { + lt: { "product:successActionButtonLabel": "MDN „Premium“ paslauga", "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "lv": { + lv: { "product:successActionButtonLabel": "MDN Premium pakalpojums", "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "nl": { + nl: { "product:successActionButtonLabel": "Doorgaan naar MDN Plus", "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "pt": { + pt: { "product:successActionButtonLabel": "Um Serviço Premium MDN", "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "sk": { + sk: { "product:successActionButtonLabel": "Služba MDN Premium", "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "sl": { + sl: { "product:successActionButtonLabel": "Storitev MDN Premium", "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "tr": { + tr: { "product:successActionButtonLabel": "Bir MDN Premium Hizmeti", "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - } + "product:details:3": "MDN çevrim dışı", + }, }, - "defaultLanguage": "pt" + defaultLanguage: "pt", }, - "CH": { - "currency": "chf", - "supportedLanguages": { - "en": null, - "de": { + CH: { + currency: "chf", + supportedLanguages: { + en: null, + de: { "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "fr": { + fr: { "product:successActionButtonLabel": "Continuer vers MDN Plus", "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "it": { + it: { "product:successActionButtonLabel": "Prosegui su MDN Plus", "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" - } + "product:details:3": "MDN offline", + }, }, - "defaultLanguage": "de" - } + defaultLanguage: "de", + }, }, - "langCurrencyToPlans": { + langCurrencyToPlans: { "usd-en": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "usd", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - USD EN", - "lookup_key": "usd-en-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "usd", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - USD EN", + lookup_key: "usd-en-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:b6f5727337fefe8e": "mdn_plus_5m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", "capabilities:ed18cbc69ec23491": "mdn_plus_5m", @@ -2087,22 +2089,22 @@ "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" - }, - "price_id": "price_1JFoTYKb9q6OnNsLalexa03p" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "usd", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - USD EN", - "lookup_key": "usd-en-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN offline", + }, + price_id: "price_1JFoTYKb9q6OnNsLalexa03p", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "usd", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - USD EN", + lookup_key: "usd-en-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:b6f5727337fefe8e": "mdn_plus_5y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", "capabilities:ed18cbc69ec23491": "mdn_plus_5y", @@ -2110,22 +2112,22 @@ "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" - }, - "price_id": "price_1JpIPwKb9q6OnNsLJLsIqMp7" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "usd", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - USD EN", - "lookup_key": "usd-en-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN offline", + }, + price_id: "price_1JpIPwKb9q6OnNsLJLsIqMp7", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "usd", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - USD EN", + lookup_key: "usd-en-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:b6f5727337fefe8e": "mdn_plus_10m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", "capabilities:ed18cbc69ec23491": "mdn_plus_10m", @@ -2133,22 +2135,22 @@ "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" - }, - "price_id": "price_1K6X7gKb9q6OnNsLi44HdLcC" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "usd", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - USD EN", - "lookup_key": "usd-en-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN offline", + }, + price_id: "price_1K6X7gKb9q6OnNsLi44HdLcC", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "usd", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - USD EN", + lookup_key: "usd-en-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:b6f5727337fefe8e": "mdn_plus_10y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", "capabilities:ed18cbc69ec23491": "mdn_plus_10y", @@ -2156,24 +2158,24 @@ "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "price_id": "price_1K6X8VKb9q6OnNsLFlUcEiu4" - } + price_id: "price_1K6X8VKb9q6OnNsLFlUcEiu4", + }, }, "eur-en": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR EN", - "lookup_key": "eur-en-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR EN", + lookup_key: "eur-en-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:b6f5727337fefe8e": "mdn_plus_5m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", "capabilities:ed18cbc69ec23491": "mdn_plus_5m", @@ -2181,22 +2183,22 @@ "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqdudKb9q6OnNsLvZkMKGkx" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR EN", - "lookup_key": "eur-en-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqdudKb9q6OnNsLvZkMKGkx", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR EN", + lookup_key: "eur-en-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:b6f5727337fefe8e": "mdn_plus_5y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", "capabilities:ed18cbc69ec23491": "mdn_plus_5y", @@ -2204,22 +2206,22 @@ "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqdudKb9q6OnNsLJAJVh0rh" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR EN", - "lookup_key": "eur-en-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqdudKb9q6OnNsLJAJVh0rh", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR EN", + lookup_key: "eur-en-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:b6f5727337fefe8e": "mdn_plus_10m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", "capabilities:ed18cbc69ec23491": "mdn_plus_10m", @@ -2227,22 +2229,22 @@ "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqdudKb9q6OnNsLDYhWPIjT" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR EN", - "lookup_key": "eur-en-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqdudKb9q6OnNsLDYhWPIjT", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR EN", + lookup_key: "eur-en-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:b6f5727337fefe8e": "mdn_plus_10y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", "capabilities:ed18cbc69ec23491": "mdn_plus_10y", @@ -2250,24 +2252,24 @@ "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "price_id": "price_1KqdueKb9q6OnNsLDg4Toybn" - } + price_id: "price_1KqdueKb9q6OnNsLDg4Toybn", + }, }, "eur-de": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR DE", - "lookup_key": "eur-de-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR DE", + lookup_key: "eur-de-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:b6f5727337fefe8e": "mdn_plus_5m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", "capabilities:ed18cbc69ec23491": "mdn_plus_5m", @@ -2275,22 +2277,22 @@ "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" - }, - "price_id": "price_1Ko6oDKb9q6OnNsL3UV65T60" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR DE", - "lookup_key": "eur-de-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN offline", + }, + price_id: "price_1Ko6oDKb9q6OnNsL3UV65T60", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR DE", + lookup_key: "eur-de-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:b6f5727337fefe8e": "mdn_plus_5y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", "capabilities:ed18cbc69ec23491": "mdn_plus_5y", @@ -2298,22 +2300,22 @@ "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" - }, - "price_id": "price_1Ko6qAKb9q6OnNsLdsHFRRYW" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR DE", - "lookup_key": "eur-de-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN offline", + }, + price_id: "price_1Ko6qAKb9q6OnNsLdsHFRRYW", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR DE", + lookup_key: "eur-de-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:b6f5727337fefe8e": "mdn_plus_10m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", "capabilities:ed18cbc69ec23491": "mdn_plus_10m", @@ -2321,22 +2323,22 @@ "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" - }, - "price_id": "price_1Ko6rsKb9q6OnNsL9jMzlpUn" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR DE", - "lookup_key": "eur-de-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN offline", + }, + price_id: "price_1Ko6rsKb9q6OnNsL9jMzlpUn", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR DE", + lookup_key: "eur-de-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:b6f5727337fefe8e": "mdn_plus_10y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", "capabilities:ed18cbc69ec23491": "mdn_plus_10y", @@ -2344,24 +2346,24 @@ "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "price_id": "price_1Ko6stKb9q6OnNsL4rnrw4Wn" - } + price_id: "price_1Ko6stKb9q6OnNsL4rnrw4Wn", + }, }, "eur-el": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR EL", - "lookup_key": "eur-el-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR EL", + lookup_key: "eur-el-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:b6f5727337fefe8e": "mdn_plus_5m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", "capabilities:ed18cbc69ec23491": "mdn_plus_5m", @@ -2369,22 +2371,22 @@ "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" - }, - "price_id": "price_1L2tOPKb9q6OnNsLvZalzbLg" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR EL", - "lookup_key": "eur-el-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "Συνέχεια σε MDN Plus", + }, + price_id: "price_1L2tOPKb9q6OnNsLvZalzbLg", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR EL", + lookup_key: "eur-el-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:b6f5727337fefe8e": "mdn_plus_5y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", "capabilities:ed18cbc69ec23491": "mdn_plus_5y", @@ -2392,22 +2394,22 @@ "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" - }, - "price_id": "price_1L2tOPKb9q6OnNsLkpUbrnJb" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR EL", - "lookup_key": "eur-el-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "Συνέχεια σε MDN Plus", + }, + price_id: "price_1L2tOPKb9q6OnNsLkpUbrnJb", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR EL", + lookup_key: "eur-el-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:b6f5727337fefe8e": "mdn_plus_10m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", "capabilities:ed18cbc69ec23491": "mdn_plus_10m", @@ -2415,22 +2417,22 @@ "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" - }, - "price_id": "price_1L2tOPKb9q6OnNsLvH7CfRM9" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR EL", - "lookup_key": "eur-el-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "Συνέχεια σε MDN Plus", + }, + price_id: "price_1L2tOPKb9q6OnNsLvH7CfRM9", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR EL", + lookup_key: "eur-el-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:b6f5727337fefe8e": "mdn_plus_10y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", "capabilities:ed18cbc69ec23491": "mdn_plus_10y", @@ -2438,24 +2440,24 @@ "product:subtitle": "Ειδοποιήσεις σελίδας", "product:details:1": "Συλλογή άρθρων", "product:details:2": "MDN εκτός σύνδεσης", - "product:details:3": "Συνέχεια σε MDN Plus" + "product:details:3": "Συνέχεια σε MDN Plus", }, - "price_id": "price_1L2tOQKb9q6OnNsLep9UMq1P" - } + price_id: "price_1L2tOQKb9q6OnNsLep9UMq1P", + }, }, "eur-es": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR ES", - "lookup_key": "eur-es-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR ES", + lookup_key: "eur-es-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:b6f5727337fefe8e": "mdn_plus_5m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", "capabilities:ed18cbc69ec23491": "mdn_plus_5m", @@ -2463,22 +2465,22 @@ "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" - }, - "price_id": "price_1KqdugKb9q6OnNsL41ATLMv6" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR ES", - "lookup_key": "eur-es-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN disponible sin conexión", + }, + price_id: "price_1KqdugKb9q6OnNsL41ATLMv6", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR ES", + lookup_key: "eur-es-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:b6f5727337fefe8e": "mdn_plus_5y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", "capabilities:ed18cbc69ec23491": "mdn_plus_5y", @@ -2486,22 +2488,22 @@ "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" - }, - "price_id": "price_1KqdugKb9q6OnNsLXv0GwhaT" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR ES", - "lookup_key": "eur-es-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN disponible sin conexión", + }, + price_id: "price_1KqdugKb9q6OnNsLXv0GwhaT", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR ES", + lookup_key: "eur-es-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:b6f5727337fefe8e": "mdn_plus_10m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", "capabilities:ed18cbc69ec23491": "mdn_plus_10m", @@ -2509,22 +2511,22 @@ "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" - }, - "price_id": "price_1KqdugKb9q6OnNsLF5ZHUiBP" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR ES", - "lookup_key": "eur-es-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN disponible sin conexión", + }, + price_id: "price_1KqdugKb9q6OnNsLF5ZHUiBP", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR ES", + lookup_key: "eur-es-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:b6f5727337fefe8e": "mdn_plus_10y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", "capabilities:ed18cbc69ec23491": "mdn_plus_10y", @@ -2532,24 +2534,24 @@ "product:subtitle": "Un servicio MDN prémium", "product:details:1": "Notificaciones de página", "product:details:2": "Colecciones de artículos", - "product:details:3": "MDN disponible sin conexión" + "product:details:3": "MDN disponible sin conexión", }, - "price_id": "price_1KqduhKb9q6OnNsLfrNPlNDh" - } + price_id: "price_1KqduhKb9q6OnNsLfrNPlNDh", + }, }, "eur-et": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR ET", - "lookup_key": "eur-et-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR ET", + lookup_key: "eur-et-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:b6f5727337fefe8e": "mdn_plus_5m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", "capabilities:ed18cbc69ec23491": "mdn_plus_5m", @@ -2557,22 +2559,22 @@ "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" - }, - "price_id": "price_1L2tOQKb9q6OnNsLYmRXAHtz" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR ET", - "lookup_key": "eur-et-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN offline", + }, + price_id: "price_1L2tOQKb9q6OnNsLYmRXAHtz", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR ET", + lookup_key: "eur-et-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:b6f5727337fefe8e": "mdn_plus_5y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", "capabilities:ed18cbc69ec23491": "mdn_plus_5y", @@ -2580,22 +2582,22 @@ "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" - }, - "price_id": "price_1L2tORKb9q6OnNsLhVlLhMsM" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR ET", - "lookup_key": "eur-et-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN offline", + }, + price_id: "price_1L2tORKb9q6OnNsLhVlLhMsM", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR ET", + lookup_key: "eur-et-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:b6f5727337fefe8e": "mdn_plus_10m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", "capabilities:ed18cbc69ec23491": "mdn_plus_10m", @@ -2603,22 +2605,22 @@ "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" - }, - "price_id": "price_1L2tORKb9q6OnNsLEqTNWbxw" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR ET", - "lookup_key": "eur-et-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN offline", + }, + price_id: "price_1L2tORKb9q6OnNsLEqTNWbxw", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR ET", + lookup_key: "eur-et-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:b6f5727337fefe8e": "mdn_plus_10y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", "capabilities:ed18cbc69ec23491": "mdn_plus_10y", @@ -2626,24 +2628,24 @@ "product:subtitle": "Lehe teavitused", "product:details:1": "Artiklite kollektsioonid", "product:details:2": "Liikuge edasi MDN Plus teenuse juurde", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "price_id": "price_1L2tOSKb9q6OnNsLdujiVlew" - } + price_id: "price_1L2tOSKb9q6OnNsLdujiVlew", + }, }, "eur-fr": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR FR", - "lookup_key": "eur-fr-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR FR", + lookup_key: "eur-fr-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:b6f5727337fefe8e": "mdn_plus_5m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", "capabilities:ed18cbc69ec23491": "mdn_plus_5m", @@ -2651,22 +2653,22 @@ "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" - }, - "price_id": "price_1KqduhKb9q6OnNsLOPbGN60q" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR FR", - "lookup_key": "eur-fr-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN hors ligne", + }, + price_id: "price_1KqduhKb9q6OnNsLOPbGN60q", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR FR", + lookup_key: "eur-fr-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:b6f5727337fefe8e": "mdn_plus_5y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", "capabilities:ed18cbc69ec23491": "mdn_plus_5y", @@ -2674,22 +2676,22 @@ "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" - }, - "price_id": "price_1KqduhKb9q6OnNsL2Iio3oSP" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR FR", - "lookup_key": "eur-fr-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN hors ligne", + }, + price_id: "price_1KqduhKb9q6OnNsL2Iio3oSP", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR FR", + lookup_key: "eur-fr-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:b6f5727337fefe8e": "mdn_plus_10m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", "capabilities:ed18cbc69ec23491": "mdn_plus_10m", @@ -2697,22 +2699,22 @@ "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" - }, - "price_id": "price_1KqduiKb9q6OnNsLzF5Ca5LV" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR FR", - "lookup_key": "eur-fr-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN hors ligne", + }, + price_id: "price_1KqduiKb9q6OnNsLzF5Ca5LV", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR FR", + lookup_key: "eur-fr-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:b6f5727337fefe8e": "mdn_plus_10y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", "capabilities:ed18cbc69ec23491": "mdn_plus_10y", @@ -2720,24 +2722,24 @@ "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "price_id": "price_1KqduiKb9q6OnNsLH0G5Lmvk" - } + price_id: "price_1KqduiKb9q6OnNsLH0G5Lmvk", + }, }, "eur-it": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR IT", - "lookup_key": "eur-it-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR IT", + lookup_key: "eur-it-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:b6f5727337fefe8e": "mdn_plus_5m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", "capabilities:ed18cbc69ec23491": "mdn_plus_5m", @@ -2745,22 +2747,22 @@ "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqduiKb9q6OnNsL6Amq5kOo" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR IT", - "lookup_key": "eur-it-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqduiKb9q6OnNsL6Amq5kOo", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR IT", + lookup_key: "eur-it-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:b6f5727337fefe8e": "mdn_plus_5y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", "capabilities:ed18cbc69ec23491": "mdn_plus_5y", @@ -2768,22 +2770,22 @@ "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqdujKb9q6OnNsLhdfctEHd" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR IT", - "lookup_key": "eur-it-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqdujKb9q6OnNsLhdfctEHd", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR IT", + lookup_key: "eur-it-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:b6f5727337fefe8e": "mdn_plus_10m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", "capabilities:ed18cbc69ec23491": "mdn_plus_10m", @@ -2791,22 +2793,22 @@ "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqdujKb9q6OnNsLPFNYmZdu" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR IT", - "lookup_key": "eur-it-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqdujKb9q6OnNsLPFNYmZdu", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR IT", + lookup_key: "eur-it-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:b6f5727337fefe8e": "mdn_plus_10y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", "capabilities:ed18cbc69ec23491": "mdn_plus_10y", @@ -2814,24 +2816,24 @@ "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "price_id": "price_1KqdukKb9q6OnNsLje4nw89h" - } + price_id: "price_1KqdukKb9q6OnNsLje4nw89h", + }, }, "eur-lt": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR LT", - "lookup_key": "eur-lt-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR LT", + lookup_key: "eur-lt-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:b6f5727337fefe8e": "mdn_plus_5m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", "capabilities:ed18cbc69ec23491": "mdn_plus_5m", @@ -2839,22 +2841,22 @@ "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" - }, - "price_id": "price_1L2tOSKb9q6OnNsLcj6DVCay" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR LT", - "lookup_key": "eur-lt-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN neprisijungus", + }, + price_id: "price_1L2tOSKb9q6OnNsLcj6DVCay", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR LT", + lookup_key: "eur-lt-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:b6f5727337fefe8e": "mdn_plus_5y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", "capabilities:ed18cbc69ec23491": "mdn_plus_5y", @@ -2862,22 +2864,22 @@ "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" - }, - "price_id": "price_1L2tOSKb9q6OnNsLMXAdyweB" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR LT", - "lookup_key": "eur-lt-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN neprisijungus", + }, + price_id: "price_1L2tOSKb9q6OnNsLMXAdyweB", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR LT", + lookup_key: "eur-lt-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:b6f5727337fefe8e": "mdn_plus_10m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", "capabilities:ed18cbc69ec23491": "mdn_plus_10m", @@ -2885,22 +2887,22 @@ "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" - }, - "price_id": "price_1L2tOTKb9q6OnNsLeLloEB9W" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR LT", - "lookup_key": "eur-lt-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN neprisijungus", + }, + price_id: "price_1L2tOTKb9q6OnNsLeLloEB9W", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR LT", + lookup_key: "eur-lt-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:b6f5727337fefe8e": "mdn_plus_10y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", "capabilities:ed18cbc69ec23491": "mdn_plus_10y", @@ -2908,24 +2910,24 @@ "product:subtitle": "Puslapio pranešimai", "product:details:1": "Straipsnių rinkiniai", "product:details:2": "Tęsti su „MDN Plus“", - "product:details:3": "MDN neprisijungus" + "product:details:3": "MDN neprisijungus", }, - "price_id": "price_1L2tOTKb9q6OnNsLtyHtZTZs" - } + price_id: "price_1L2tOTKb9q6OnNsLtyHtZTZs", + }, }, "eur-lv": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR LV", - "lookup_key": "eur-lv-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR LV", + lookup_key: "eur-lv-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:b6f5727337fefe8e": "mdn_plus_5m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", "capabilities:ed18cbc69ec23491": "mdn_plus_5m", @@ -2933,22 +2935,22 @@ "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" - }, - "price_id": "price_1L2tOTKb9q6OnNsLavSzI6Rc" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR LV", - "lookup_key": "eur-lv-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN bezsaistē", + }, + price_id: "price_1L2tOTKb9q6OnNsLavSzI6Rc", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR LV", + lookup_key: "eur-lv-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:b6f5727337fefe8e": "mdn_plus_5y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", "capabilities:ed18cbc69ec23491": "mdn_plus_5y", @@ -2956,22 +2958,22 @@ "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" - }, - "price_id": "price_1L2tOUKb9q6OnNsLkzLwkTdw" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR LV", - "lookup_key": "eur-lv-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN bezsaistē", + }, + price_id: "price_1L2tOUKb9q6OnNsLkzLwkTdw", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR LV", + lookup_key: "eur-lv-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:b6f5727337fefe8e": "mdn_plus_10m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", "capabilities:ed18cbc69ec23491": "mdn_plus_10m", @@ -2979,22 +2981,22 @@ "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" - }, - "price_id": "price_1L2tOUKb9q6OnNsLLJLA6cln" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR LV", - "lookup_key": "eur-lv-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN bezsaistē", + }, + price_id: "price_1L2tOUKb9q6OnNsLLJLA6cln", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR LV", + lookup_key: "eur-lv-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:b6f5727337fefe8e": "mdn_plus_10y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", "capabilities:ed18cbc69ec23491": "mdn_plus_10y", @@ -3002,24 +3004,24 @@ "product:subtitle": "Lapas paziņojumi", "product:details:1": "Rakstu apkopojumi", "product:details:2": "Turpināt ar MDN Plus", - "product:details:3": "MDN bezsaistē" + "product:details:3": "MDN bezsaistē", }, - "price_id": "price_1L2tOUKb9q6OnNsLV6ORmdfX" - } + price_id: "price_1L2tOUKb9q6OnNsLV6ORmdfX", + }, }, "eur-nl": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR NL", - "lookup_key": "eur-nl-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR NL", + lookup_key: "eur-nl-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:b6f5727337fefe8e": "mdn_plus_5m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", "capabilities:ed18cbc69ec23491": "mdn_plus_5m", @@ -3027,22 +3029,22 @@ "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqdukKb9q6OnNsLjjA8mTpM" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR NL", - "lookup_key": "eur-nl-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqdukKb9q6OnNsLjjA8mTpM", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR NL", + lookup_key: "eur-nl-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:b6f5727337fefe8e": "mdn_plus_5y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", "capabilities:ed18cbc69ec23491": "mdn_plus_5y", @@ -3050,22 +3052,22 @@ "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqdukKb9q6OnNsLPCtBqPcw" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR NL", - "lookup_key": "eur-nl-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqdukKb9q6OnNsLPCtBqPcw", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR NL", + lookup_key: "eur-nl-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:b6f5727337fefe8e": "mdn_plus_10m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", "capabilities:ed18cbc69ec23491": "mdn_plus_10m", @@ -3073,22 +3075,22 @@ "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqdulKb9q6OnNsLWgRvCo2X" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR NL", - "lookup_key": "eur-nl-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqdulKb9q6OnNsLWgRvCo2X", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR NL", + lookup_key: "eur-nl-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:b6f5727337fefe8e": "mdn_plus_10y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", "capabilities:ed18cbc69ec23491": "mdn_plus_10y", @@ -3096,24 +3098,24 @@ "product:subtitle": "Een MDN Premium-service", "product:details:1": "Paginameldingen", "product:details:2": "Verzamelingen artikelen", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "price_id": "price_1KqdulKb9q6OnNsLnNtjAc1b" - } + price_id: "price_1KqdulKb9q6OnNsLnNtjAc1b", + }, }, "eur-pt": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR PT", - "lookup_key": "eur-pt-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR PT", + lookup_key: "eur-pt-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:b6f5727337fefe8e": "mdn_plus_5m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", "capabilities:ed18cbc69ec23491": "mdn_plus_5m", @@ -3121,22 +3123,22 @@ "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" - }, - "price_id": "price_1L2tOVKb9q6OnNsLKEZeiAUa" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR PT", - "lookup_key": "eur-pt-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN Offline", + }, + price_id: "price_1L2tOVKb9q6OnNsLKEZeiAUa", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR PT", + lookup_key: "eur-pt-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:b6f5727337fefe8e": "mdn_plus_5y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", "capabilities:ed18cbc69ec23491": "mdn_plus_5y", @@ -3144,22 +3146,22 @@ "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" - }, - "price_id": "price_1L2tOVKb9q6OnNsLlL79fx7O" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR PT", - "lookup_key": "eur-pt-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN Offline", + }, + price_id: "price_1L2tOVKb9q6OnNsLlL79fx7O", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR PT", + lookup_key: "eur-pt-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:b6f5727337fefe8e": "mdn_plus_10m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", "capabilities:ed18cbc69ec23491": "mdn_plus_10m", @@ -3167,22 +3169,22 @@ "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" - }, - "price_id": "price_1L2tOWKb9q6OnNsLlvcFItux" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR PT", - "lookup_key": "eur-pt-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN Offline", + }, + price_id: "price_1L2tOWKb9q6OnNsLlvcFItux", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR PT", + lookup_key: "eur-pt-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:b6f5727337fefe8e": "mdn_plus_10y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", "capabilities:ed18cbc69ec23491": "mdn_plus_10y", @@ -3190,24 +3192,24 @@ "product:subtitle": "Notificações de página", "product:details:1": "Coleções de artigos", "product:details:2": "Continuar para o MDN Plus", - "product:details:3": "MDN Offline" + "product:details:3": "MDN Offline", }, - "price_id": "price_1L2tOWKb9q6OnNsLeDlMYfzM" - } + price_id: "price_1L2tOWKb9q6OnNsLeDlMYfzM", + }, }, "eur-sk": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR SK", - "lookup_key": "eur-sk-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR SK", + lookup_key: "eur-sk-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:b6f5727337fefe8e": "mdn_plus_5m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", "capabilities:ed18cbc69ec23491": "mdn_plus_5m", @@ -3215,22 +3217,22 @@ "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" - }, - "price_id": "price_1L2tOXKb9q6OnNsLgdMHi7aT" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR SK", - "lookup_key": "eur-sk-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN offline", + }, + price_id: "price_1L2tOXKb9q6OnNsLgdMHi7aT", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR SK", + lookup_key: "eur-sk-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:b6f5727337fefe8e": "mdn_plus_5y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", "capabilities:ed18cbc69ec23491": "mdn_plus_5y", @@ -3238,22 +3240,22 @@ "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" - }, - "price_id": "price_1L2tOXKb9q6OnNsLY1NOhF8J" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR SK", - "lookup_key": "eur-sk-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN offline", + }, + price_id: "price_1L2tOXKb9q6OnNsLY1NOhF8J", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR SK", + lookup_key: "eur-sk-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:b6f5727337fefe8e": "mdn_plus_10m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", "capabilities:ed18cbc69ec23491": "mdn_plus_10m", @@ -3261,22 +3263,22 @@ "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" - }, - "price_id": "price_1L2tOXKb9q6OnNsL0USQVLnk" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR SK", - "lookup_key": "eur-sk-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN offline", + }, + price_id: "price_1L2tOXKb9q6OnNsL0USQVLnk", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR SK", + lookup_key: "eur-sk-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:b6f5727337fefe8e": "mdn_plus_10y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", "capabilities:ed18cbc69ec23491": "mdn_plus_10y", @@ -3284,24 +3286,24 @@ "product:subtitle": "Upozornenia na stránke", "product:details:1": "Zbierky článkov", "product:details:2": "Pokračujte na MDN Plus", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "price_id": "price_1L2tOYKb9q6OnNsLLN78izjz" - } + price_id: "price_1L2tOYKb9q6OnNsLLN78izjz", + }, }, "eur-sl": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR SL", - "lookup_key": "eur-sl-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR SL", + lookup_key: "eur-sl-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:b6f5727337fefe8e": "mdn_plus_5m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", "capabilities:ed18cbc69ec23491": "mdn_plus_5m", @@ -3309,22 +3311,22 @@ "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" - }, - "price_id": "price_1L2tOYKb9q6OnNsL0FTDUmNL" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR SL", - "lookup_key": "eur-sl-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN brez povezave", + }, + price_id: "price_1L2tOYKb9q6OnNsL0FTDUmNL", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR SL", + lookup_key: "eur-sl-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:b6f5727337fefe8e": "mdn_plus_5y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", "capabilities:ed18cbc69ec23491": "mdn_plus_5y", @@ -3332,22 +3334,22 @@ "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" - }, - "price_id": "price_1L2tOZKb9q6OnNsLVUOKdstR" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR SL", - "lookup_key": "eur-sl-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN brez povezave", + }, + price_id: "price_1L2tOZKb9q6OnNsLVUOKdstR", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR SL", + lookup_key: "eur-sl-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:b6f5727337fefe8e": "mdn_plus_10m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", "capabilities:ed18cbc69ec23491": "mdn_plus_10m", @@ -3355,22 +3357,22 @@ "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" - }, - "price_id": "price_1L2tOZKb9q6OnNsL73fEBYE0" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR SL", - "lookup_key": "eur-sl-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN brez povezave", + }, + price_id: "price_1L2tOZKb9q6OnNsL73fEBYE0", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR SL", + lookup_key: "eur-sl-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:b6f5727337fefe8e": "mdn_plus_10y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", "capabilities:ed18cbc69ec23491": "mdn_plus_10y", @@ -3378,24 +3380,24 @@ "product:subtitle": "Obvestila strani", "product:details:1": "Zbirke člankov", "product:details:2": "Nadaljuj na MDN Plus", - "product:details:3": "MDN brez povezave" + "product:details:3": "MDN brez povezave", }, - "price_id": "price_1L2tOaKb9q6OnNsLroZYuVLp" - } + price_id: "price_1L2tOaKb9q6OnNsLroZYuVLp", + }, }, "eur-tr": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - EUR TR", - "lookup_key": "eur-tr-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - EUR TR", + lookup_key: "eur-tr-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:b6f5727337fefe8e": "mdn_plus_5m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", "capabilities:ed18cbc69ec23491": "mdn_plus_5m", @@ -3403,22 +3405,22 @@ "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - }, - "price_id": "price_1L2tOaKb9q6OnNsLawGUUtNl" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - EUR TR", - "lookup_key": "eur-tr-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN çevrim dışı", + }, + price_id: "price_1L2tOaKb9q6OnNsLawGUUtNl", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - EUR TR", + lookup_key: "eur-tr-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:b6f5727337fefe8e": "mdn_plus_5y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", "capabilities:ed18cbc69ec23491": "mdn_plus_5y", @@ -3426,22 +3428,22 @@ "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - }, - "price_id": "price_1L2tOaKb9q6OnNsL3LhuZ3lY" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - EUR TR", - "lookup_key": "eur-tr-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN çevrim dışı", + }, + price_id: "price_1L2tOaKb9q6OnNsL3LhuZ3lY", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - EUR TR", + lookup_key: "eur-tr-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:b6f5727337fefe8e": "mdn_plus_10m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", "capabilities:ed18cbc69ec23491": "mdn_plus_10m", @@ -3449,22 +3451,22 @@ "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" - }, - "price_id": "price_1L2tObKb9q6OnNsLtuAwE66M" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "eur", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - EUR TR", - "lookup_key": "eur-tr-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN çevrim dışı", + }, + price_id: "price_1L2tObKb9q6OnNsLtuAwE66M", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "eur", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - EUR TR", + lookup_key: "eur-tr-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:b6f5727337fefe8e": "mdn_plus_10y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", "capabilities:ed18cbc69ec23491": "mdn_plus_10y", @@ -3472,24 +3474,24 @@ "product:subtitle": "Sayfa bildirimleri", "product:details:1": "Toplanmış makaleler", "product:details:2": "MDN Plus'a devam edin", - "product:details:3": "MDN çevrim dışı" + "product:details:3": "MDN çevrim dışı", }, - "price_id": "price_1L2tObKb9q6OnNsLdE48ccAa" - } + price_id: "price_1L2tObKb9q6OnNsLdE48ccAa", + }, }, "chf-en": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "chf", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - CHF EN", - "lookup_key": "chf-en-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "chf", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - CHF EN", + lookup_key: "chf-en-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:b6f5727337fefe8e": "mdn_plus_5m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", "capabilities:ed18cbc69ec23491": "mdn_plus_5m", @@ -3497,22 +3499,22 @@ "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqdulKb9q6OnNsLbkK1T4dR" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "chf", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - CHF EN", - "lookup_key": "chf-en-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqdulKb9q6OnNsLbkK1T4dR", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "chf", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - CHF EN", + lookup_key: "chf-en-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:b6f5727337fefe8e": "mdn_plus_5y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", "capabilities:ed18cbc69ec23491": "mdn_plus_5y", @@ -3520,22 +3522,22 @@ "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqdumKb9q6OnNsLfjFLaTzi" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "chf", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - CHF EN", - "lookup_key": "chf-en-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqdumKb9q6OnNsLfjFLaTzi", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "chf", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - CHF EN", + lookup_key: "chf-en-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:b6f5727337fefe8e": "mdn_plus_10m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", "capabilities:ed18cbc69ec23491": "mdn_plus_10m", @@ -3543,22 +3545,22 @@ "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqdumKb9q6OnNsLc248nWtE" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "chf", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - CHF EN", - "lookup_key": "chf-en-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqdumKb9q6OnNsLc248nWtE", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "chf", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - CHF EN", + lookup_key: "chf-en-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:b6f5727337fefe8e": "mdn_plus_10y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", "capabilities:ed18cbc69ec23491": "mdn_plus_10y", @@ -3566,24 +3568,24 @@ "product:subtitle": "An MDN Premium Service", "product:details:1": "Page notifications", "product:details:2": "Collections of articles", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "price_id": "price_1KqdumKb9q6OnNsLEbRL4nAo" - } + price_id: "price_1KqdumKb9q6OnNsLEbRL4nAo", + }, }, "chf-de": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "chf", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - CHF DE", - "lookup_key": "chf-de-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "chf", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - CHF DE", + lookup_key: "chf-de-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:b6f5727337fefe8e": "mdn_plus_5m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", "capabilities:ed18cbc69ec23491": "mdn_plus_5m", @@ -3591,22 +3593,22 @@ "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqdunKb9q6OnNsLMgnnIma0" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "chf", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - CHF DE", - "lookup_key": "chf-de-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqdunKb9q6OnNsLMgnnIma0", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "chf", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - CHF DE", + lookup_key: "chf-de-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:b6f5727337fefe8e": "mdn_plus_5y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", "capabilities:ed18cbc69ec23491": "mdn_plus_5y", @@ -3614,22 +3616,22 @@ "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqdunKb9q6OnNsLiF9qvEVf" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "chf", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - CHF DE", - "lookup_key": "chf-de-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqdunKb9q6OnNsLiF9qvEVf", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "chf", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - CHF DE", + lookup_key: "chf-de-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:b6f5727337fefe8e": "mdn_plus_10m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", "capabilities:ed18cbc69ec23491": "mdn_plus_10m", @@ -3637,47 +3639,47 @@ "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqdunKb9q6OnNsLnaEHicOP" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "chf", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - CHF DE", - "lookup_key": "chf-de-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqdunKb9q6OnNsLnaEHicOP", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "chf", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - CHF DE", + lookup_key: "chf-de-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:b6f5727337fefe8e": "mdn_plus_10y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", "capabilities:ed18cbc69ec23491": "mdn_plus_10y", "product:successActionButtonLabel": "Weiter zu MDN Plus", "product:subtitle": "Ein MDN-Premium-Dienst", "product:details:1": "Seitenbenachrichtigungen", - "product:details:2": "Artikelsammlung", - "product:details:3": "MDN offline" + "product:de;tails:2": "Artikelsammlung", + "product:details:3": "MDN offline", }, - "price_id": "price_1KqduoKb9q6OnNsLQrxviGu0" - } + price_id: "price_1KqduoKb9q6OnNsLQrxviGu0", + }, }, "chf-fr": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "chf", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - CHF FR", - "lookup_key": "chf-fr-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "chf", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - CHF FR", + lookup_key: "chf-fr-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:b6f5727337fefe8e": "mdn_plus_5m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", "capabilities:ed18cbc69ec23491": "mdn_plus_5m", @@ -3685,22 +3687,22 @@ "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" - }, - "price_id": "price_1KqduoKb9q6OnNsLv4AoSQJg" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "chf", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - CHF FR", - "lookup_key": "chf-fr-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN hors ligne", + }, + price_id: "price_1KqduoKb9q6OnNsLv4AoSQJg", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "chf", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - CHF FR", + lookup_key: "chf-fr-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:b6f5727337fefe8e": "mdn_plus_5y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", "capabilities:ed18cbc69ec23491": "mdn_plus_5y", @@ -3708,22 +3710,22 @@ "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" - }, - "price_id": "price_1KqduoKb9q6OnNsLf9iSMYIf" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "chf", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - CHF FR", - "lookup_key": "chf-fr-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN hors ligne", + }, + price_id: "price_1KqduoKb9q6OnNsLf9iSMYIf", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "chf", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - CHF FR", + lookup_key: "chf-fr-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:b6f5727337fefe8e": "mdn_plus_10m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", "capabilities:ed18cbc69ec23491": "mdn_plus_10m", @@ -3731,22 +3733,22 @@ "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" - }, - "price_id": "price_1KqdupKb9q6OnNsLWiJvcYUp" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "chf", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - CHF FR", - "lookup_key": "chf-fr-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN hors ligne", + }, + price_id: "price_1KqdupKb9q6OnNsLWiJvcYUp", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "chf", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - CHF FR", + lookup_key: "chf-fr-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:b6f5727337fefe8e": "mdn_plus_10y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", "capabilities:ed18cbc69ec23491": "mdn_plus_10y", @@ -3754,24 +3756,24 @@ "product:subtitle": "Un service MDN Premium", "product:details:1": "Notifications de la page", "product:details:2": "Collections d’articles", - "product:details:3": "MDN hors ligne" + "product:details:3": "MDN hors ligne", }, - "price_id": "price_1KqdupKb9q6OnNsLcVJfbhL9" - } + price_id: "price_1KqdupKb9q6OnNsLcVJfbhL9", + }, }, "chf-it": { - "mdn_plus_5m": { - "unit_amount": 500, - "currency": "chf", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 5m - CHF IT", - "lookup_key": "chf-it-5m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "1", + mdn_plus_5m: { + unit_amount: 500, + currency: "chf", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 5m - CHF IT", + lookup_key: "chf-it-5m", + metadata: { + productSet: "mdnPlus", + productOrder: "1", "capabilities:b6f5727337fefe8e": "mdn_plus_5m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5m", "capabilities:ed18cbc69ec23491": "mdn_plus_5m", @@ -3779,22 +3781,22 @@ "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqdupKb9q6OnNsL1TqMroUf" - }, - "mdn_plus_5y": { - "unit_amount": 5000, - "currency": "chf", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 5y - CHF IT", - "lookup_key": "chf-it-5y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "2", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqdupKb9q6OnNsL1TqMroUf", + }, + mdn_plus_5y: { + unit_amount: 5000, + currency: "chf", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 5y - CHF IT", + lookup_key: "chf-it-5y", + metadata: { + productSet: "mdnPlus", + productOrder: "2", "capabilities:b6f5727337fefe8e": "mdn_plus_5y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_5y", "capabilities:ed18cbc69ec23491": "mdn_plus_5y", @@ -3802,22 +3804,22 @@ "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqduqKb9q6OnNsLhNwOl77S" - }, - "mdn_plus_10m": { - "unit_amount": 1000, - "currency": "chf", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "month" - }, - "nickname": "MDN Plus 10m - CHF IT", - "lookup_key": "chf-it-10m", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "3", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqduqKb9q6OnNsLhNwOl77S", + }, + mdn_plus_10m: { + unit_amount: 1000, + currency: "chf", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "month", + }, + nickname: "MDN Plus 10m - CHF IT", + lookup_key: "chf-it-10m", + metadata: { + productSet: "mdnPlus", + productOrder: "3", "capabilities:b6f5727337fefe8e": "mdn_plus_10m", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10m", "capabilities:ed18cbc69ec23491": "mdn_plus_10m", @@ -3825,22 +3827,22 @@ "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" - }, - "price_id": "price_1KqduqKb9q6OnNsLZ9yMm4To" - }, - "mdn_plus_10y": { - "unit_amount": 10000, - "currency": "chf", - "product": "prod_Jtbg9tyGyLRuB0", - "recurring": { - "interval": "year" - }, - "nickname": "MDN Plus 10y - CHF IT", - "lookup_key": "chf-it-10y", - "metadata": { - "productSet": "mdnPlus", - "productOrder": "4", + "product:details:3": "MDN offline", + }, + price_id: "price_1KqduqKb9q6OnNsLZ9yMm4To", + }, + mdn_plus_10y: { + unit_amount: 10000, + currency: "chf", + product: "prod_Jtbg9tyGyLRuB0", + recurring: { + interval: "year", + }, + nickname: "MDN Plus 10y - CHF IT", + lookup_key: "chf-it-10y", + metadata: { + productSet: "mdnPlus", + productOrder: "4", "capabilities:b6f5727337fefe8e": "mdn_plus_10y", "capabilities:e7bc284c2d3b4a90": "mdn_plus_10y", "capabilities:ed18cbc69ec23491": "mdn_plus_10y", @@ -3848,10 +3850,12 @@ "product:subtitle": "Un servizio Premium MDN", "product:details:1": "Notifiche delle pagine", "product:details:2": "Raccolte di articoli", - "product:details:3": "MDN offline" + "product:details:3": "MDN offline", }, - "price_id": "price_1KqdurKb9q6OnNsLUOz4fnk3" - } - } - } -} + price_id: "price_1KqdurKb9q6OnNsLUOz4fnk3", + }, + }, + }, +}; + +export default plans; From f513ac57f177fa4c914417c4017ed454e11c904c Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 23 Mar 2023 00:14:51 +0100 Subject: [PATCH 102/343] refactor(gcp/function): migrate to http-proxy-middleware --- gcp/function/package-lock.json | 120 ++++++++++++++++++++++++- gcp/function/package.json | 2 +- gcp/function/src/handlers/bcdApi.ts | 9 +- gcp/function/src/handlers/content.ts | 10 +-- gcp/function/src/handlers/rumba.ts | 10 +-- gcp/function/src/handlers/telemetry.ts | 9 +- 6 files changed, 132 insertions(+), 28 deletions(-) diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index cf353b2b05d7..bdbe7341b426 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -18,7 +18,7 @@ "accept-language-parser": "^1.5.0", "dotenv": "^16.0.3", "express": "^4.18.2", - "http-proxy": "^1.18.1", + "http-proxy-middleware": "^2.0.6", "sanitize-filename": "^1.6.3" }, "devDependencies": { @@ -464,7 +464,6 @@ "version": "1.17.10", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.10.tgz", "integrity": "sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g==", - "dev": true, "dependencies": { "@types/node": "*" } @@ -665,6 +664,17 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1043,6 +1053,17 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", @@ -1331,6 +1352,29 @@ "node": ">=8.0.0" } }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1468,6 +1512,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-generator-function": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", @@ -1482,6 +1534,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -1494,6 +1557,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-number-object": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", @@ -1509,6 +1580,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -1724,6 +1806,18 @@ "node": ">= 0.6" } }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -2013,6 +2107,17 @@ "node": ">=4" } }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pidtree": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", @@ -2444,6 +2549,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", diff --git a/gcp/function/package.json b/gcp/function/package.json index 1d5ee21c12f2..f9a4ec2f3c81 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -27,7 +27,7 @@ "accept-language-parser": "^1.5.0", "dotenv": "^16.0.3", "express": "^4.18.2", - "http-proxy": "^1.18.1", + "http-proxy-middleware": "^2.0.6", "sanitize-filename": "^1.6.3" }, "devDependencies": { diff --git a/gcp/function/src/handlers/bcdApi.ts b/gcp/function/src/handlers/bcdApi.ts index 200917808e3d..e8dbee769e94 100644 --- a/gcp/function/src/handlers/bcdApi.ts +++ b/gcp/function/src/handlers/bcdApi.ts @@ -1,16 +1,15 @@ import type * as express from "express"; -import httpProxy from "http-proxy"; +import { createProxyMiddleware } from "http-proxy-middleware"; + import { Source, sourceUri } from "../env.js"; import { withProxyResponseHeaders } from "../headers.js"; export function proxyBcdApi(): express.Handler { - const bcdProxy = httpProxy.createProxy({ + return createProxyMiddleware({ prependPath: true, changeOrigin: true, target: sourceUri(Source.bcdApi), autoRewrite: true, + onProxyRes: withProxyResponseHeaders, }); - bcdProxy.on("proxyRes", withProxyResponseHeaders); - - return (req, res) => bcdProxy.web(req, res); } diff --git a/gcp/function/src/handlers/content.ts b/gcp/function/src/handlers/content.ts index 8beb8e5f6eb9..5f68f506a735 100644 --- a/gcp/function/src/handlers/content.ts +++ b/gcp/function/src/handlers/content.ts @@ -1,17 +1,15 @@ import type express from "express"; +import { createProxyMiddleware } from "http-proxy-middleware"; + import { withProxyResponseHeaders } from "../headers.js"; import { Source, sourceUri } from "../env.js"; -import httpProxy from "http-proxy"; export function createContentProxy(): express.Handler { - const contentProxy = httpProxy.createProxy({ + return createProxyMiddleware({ prependPath: true, changeOrigin: true, target: sourceUri(Source.content), autoRewrite: true, + onProxyRes: withProxyResponseHeaders, }); - contentProxy.on("proxyRes", withProxyResponseHeaders); - return (req, res) => { - contentProxy.web(req, res); - }; } diff --git a/gcp/function/src/handlers/rumba.ts b/gcp/function/src/handlers/rumba.ts index 10e0e98e7530..fdc444ff1bf3 100644 --- a/gcp/function/src/handlers/rumba.ts +++ b/gcp/function/src/handlers/rumba.ts @@ -1,13 +1,9 @@ -import httpProxy from "http-proxy"; -import type express from "express"; +import { createProxyMiddleware } from "http-proxy-middleware"; + import { Source, sourceUri } from "../env.js"; -const rumbaProxy = httpProxy.createProxy({ +export const proxyRumba = createProxyMiddleware({ target: sourceUri(Source.rumba), changeOrigin: true, autoRewrite: true, }); - -export function proxyRumba(req: express.Request, res: express.Response) { - rumbaProxy.web(req, res); -} diff --git a/gcp/function/src/handlers/telemetry.ts b/gcp/function/src/handlers/telemetry.ts index 2e0c38350af0..6a50fe69803b 100644 --- a/gcp/function/src/handlers/telemetry.ts +++ b/gcp/function/src/handlers/telemetry.ts @@ -1,12 +1,7 @@ -import httpProxy from "http-proxy"; -import type express from "express"; +import { createProxyMiddleware } from "http-proxy-middleware"; -const telemetryProxy = httpProxy.createProxy({ +export const proxyTelemetry = createProxyMiddleware({ target: "https://incoming.telemetry.mozilla.org", changeOrigin: true, autoRewrite: true, }); - -export function proxyTelemetry(req: express.Request, res: express.Response) { - telemetryProxy.web(req, res); -} From dc60c6775d8de565c99a57d56a9cf3c834c455e6 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 23 Mar 2023 00:49:54 +0100 Subject: [PATCH 103/343] fix(gcp/function): add req.originalUrl workaround --- gcp/function/src/middlewares/resolveIndexHTML.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gcp/function/src/middlewares/resolveIndexHTML.ts b/gcp/function/src/middlewares/resolveIndexHTML.ts index 59acae9189c7..2f59b0763083 100644 --- a/gcp/function/src/middlewares/resolveIndexHTML.ts +++ b/gcp/function/src/middlewares/resolveIndexHTML.ts @@ -11,6 +11,9 @@ export function resolveIndexHTML( if (path.extname(resolvedUrl) === "") { resolvedUrl = path.join(resolvedUrl, "index.html"); } - req.url = resolvedUrl; + req.originalUrl = req.url = resolvedUrl; + // Workaround for http-proxy-middleware v2 using `req.originalUrl`. + // See: https://github.com/chimurai/http-proxy-middleware/pull/731 + req.originalUrl = req.url; next(); } From 0db5220da701f7fcbcf89d4db11fd2979abfe15a Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 23 Mar 2023 01:20:45 +0100 Subject: [PATCH 104/343] feat(gcp/function): handle 404 in content --- gcp/function/src/handlers/content.ts | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/gcp/function/src/handlers/content.ts b/gcp/function/src/handlers/content.ts index 5f68f506a735..5fff1c65e025 100644 --- a/gcp/function/src/handlers/content.ts +++ b/gcp/function/src/handlers/content.ts @@ -1,15 +1,33 @@ import type express from "express"; -import { createProxyMiddleware } from "http-proxy-middleware"; +import { + createProxyMiddleware, + responseInterceptor, +} from "http-proxy-middleware"; import { withProxyResponseHeaders } from "../headers.js"; import { Source, sourceUri } from "../env.js"; +const NOT_FOUND_PATH = "en-us/_spas/404.html"; + export function createContentProxy(): express.Handler { + const target = sourceUri(Source.content); return createProxyMiddleware({ prependPath: true, changeOrigin: true, - target: sourceUri(Source.content), + target, autoRewrite: true, - onProxyRes: withProxyResponseHeaders, + selfHandleResponse: true, + onProxyRes: responseInterceptor( + async (responseBuffer, proxyRes, req, res) => { + withProxyResponseHeaders(proxyRes, req, res); + if (proxyRes.statusCode === 404) { + const response = await fetch(`${target}${NOT_FOUND_PATH}`); + res.setHeader("Content-Type", "text/html"); + return Buffer.from(await response.arrayBuffer()); + } + + return responseBuffer; + } + ), }); } From 40ad45c09b04c481428ba7c46bdc55bb9b64e004 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 23 Mar 2023 01:33:37 +0100 Subject: [PATCH 105/343] Revert "chore(gcp/function): make libs copy read-only" This reverts commit 43ee2f1803e956bcbc2ed732dd6add295b5ac020. --- gcp/function/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/function/package.json b/gcp/function/package.json index f9a4ec2f3c81..b1ca3fb0df38 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -11,7 +11,7 @@ "build": "tsc -b && ts-node src/build.ts", "build-deploy-clean": "npm-run-all -s build deploy clean", "clean": "tsc -b --clean && git checkout -- redirects.json", - "copy-internal": "rm -rf ./src/internal && cp -R ../../libs ./src/internal && chmod -R -w ./src/internal", + "copy-internal": "rm -rf ./src/internal && cp -R ../../libs ./src/internal", "deploy": "gcloud functions deploy mdn-two --region europe-west3 --runtime nodejs18 --trigger-http --allow-unauthenticated", "package": "zip -r -X f.zip . -i package.json .env 'src/*.js' 'src/handlers/*.js' 'src/internal'", "prepare": "[ ! -e ../../libs ] || npm run copy-internal", From 7e6ae337af825360f33c9d29af25da21603c9c5c Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 23 Mar 2023 22:11:26 +0100 Subject: [PATCH 106/343] chore(gcp/function): remove /bcd handling --- gcp/function/src/app.ts | 2 -- gcp/function/src/env.ts | 5 ----- gcp/function/src/handlers/bcdApi.ts | 15 --------------- 3 files changed, 22 deletions(-) delete mode 100644 gcp/function/src/handlers/bcdApi.ts diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index b3313fed3b4e..d23b92ba474a 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -2,7 +2,6 @@ import express from "express"; import { Router } from "express"; import { Origin, origin } from "./env.js"; import { createContentProxy } from "./handlers/content.js"; -import { proxyBcdApi } from "./handlers/bcdApi.js"; import { proxyKevel } from "./handlers/kevel.js"; import { proxyRumba } from "./handlers/rumba.js"; import { plans } from "./handlers/plans.js"; @@ -18,7 +17,6 @@ import { redirectTrailingSlash } from "./middlewares/redirectTrailingSlash.js"; const mainRouter = Router(); const proxyContent = createContentProxy(); mainRouter.use(redirectLeadingSlash); -mainRouter.get("/bcd/api/*", proxyBcdApi()); mainRouter.all("/api/v1/stripe/plans", plans); mainRouter.all("/plus/plans.json", plans); mainRouter.all("/api/*", proxyRumba); diff --git a/gcp/function/src/env.ts b/gcp/function/src/env.ts index 884d846df8ce..e0d79ad5e674 100644 --- a/gcp/function/src/env.ts +++ b/gcp/function/src/env.ts @@ -16,7 +16,6 @@ export enum Origin { export enum Source { content = "content", liveSamples = "liveSamples", - bcdApi = "bcdApi", rumba = "rumba", } @@ -41,8 +40,6 @@ export const SOURCE_CONTENT: string = export const SOURCE_LIVE_SAMPLES: string = process.env["SOURCE_LIVE_SAMPLES"] || "https://yari-demos.prod.mdn.mozit.cloud"; -export const SOURCE_BCD_API: string = - process.env["SOURCE_BCD_API"] || "https://bcd.developer.mozilla.org"; export const SOURCE_RUMBA: string = process.env["SOURCE_RUMBA"] || "https://developer.mozilla.org"; @@ -50,8 +47,6 @@ export function sourceUri(source: Source): string { switch (source) { case Source.content: return SOURCE_CONTENT; - case Source.bcdApi: - return SOURCE_BCD_API; case Source.liveSamples: return SOURCE_LIVE_SAMPLES; case Source.rumba: diff --git a/gcp/function/src/handlers/bcdApi.ts b/gcp/function/src/handlers/bcdApi.ts deleted file mode 100644 index e8dbee769e94..000000000000 --- a/gcp/function/src/handlers/bcdApi.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type * as express from "express"; -import { createProxyMiddleware } from "http-proxy-middleware"; - -import { Source, sourceUri } from "../env.js"; -import { withProxyResponseHeaders } from "../headers.js"; - -export function proxyBcdApi(): express.Handler { - return createProxyMiddleware({ - prependPath: true, - changeOrigin: true, - target: sourceUri(Source.bcdApi), - autoRewrite: true, - onProxyRes: withProxyResponseHeaders, - }); -} From f86fed5878d9905a0bada51ce39504bb577c06b8 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 23 Mar 2023 22:12:56 +0100 Subject: [PATCH 107/343] chore(gcp/function): stick with /api/v1/stripe/plans --- gcp/function/src/app.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index d23b92ba474a..93c4ca8c0cd1 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -18,7 +18,6 @@ const mainRouter = Router(); const proxyContent = createContentProxy(); mainRouter.use(redirectLeadingSlash); mainRouter.all("/api/v1/stripe/plans", plans); -mainRouter.all("/plus/plans.json", plans); mainRouter.all("/api/*", proxyRumba); mainRouter.all("/admin-api/*", proxyRumba); mainRouter.all("/events/fxa/*", proxyRumba); From 58b57c5994ab81e10e676f52433c8504b8e54486 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 23 Mar 2023 22:17:01 +0100 Subject: [PATCH 108/343] ci(xyz-build): remove build temporarily --- .github/workflows/xyz-build.yml | 84 --------------------------------- 1 file changed, 84 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index ff83440315f7..b965e75b67d1 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -64,87 +64,6 @@ jobs: - name: Print information about CPU run: cat /proc/cpuinfo - - name: Build everything - env: - # Remember, the mdn/content repo got cloned into `pwd` into a - # sub-folder called "mdn/content" - CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files - CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files - CONTRIBUTOR_SPOTLIGHT_ROOT: ${{ github.workspace }}/mdn/mdn-contributor-spotlight/contributors - - # The default for this environment variable is geared for writers - # (aka. local development). Usually defaults are supposed to be for - # secure production but this is an exception and default - # is not insecure. - BUILD_LIVE_SAMPLES_BASE_URL: https://yari-demos.developer.allizom.xyz - - # Use the stage version of interactive examples. - BUILD_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net - - # Now is not the time to worry about flaws. - BUILD_FLAW_LEVELS: "*:ignore" - - # This is the Google Analytics account ID for developer.mozilla.org - # If it's used on other domains (e.g. stage or dev builds), it's OK - # because ultimately Google Analytics will filter it out since the - # origin domain isn't what that account expects. - #BUILD_GOOGLE_ANALYTICS_ACCOUNT: UA-36116321-5 - - # This enables the Plus call-to-action banner and the Plus landing page - REACT_APP_ENABLE_PLUS: true - - # This adds the ability to sign in (stage only for now) - REACT_APP_DISABLE_AUTH: false - - # Use the stage version of interactive examples in react app - REACT_APP_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net - - # Firefox Accounts and SubPlat settings - REACT_APP_FXA_SIGNIN_URL: /users/fxa/login/authenticate/ - REACT_APP_FXA_SETTINGS_URL: https://accounts.stage.mozaws.net/settings/ - REACT_APP_MDN_PLUS_SUBSCRIBE_URL: https://accounts.stage.mozaws.net/subscriptions/products/prod_Jtbg9tyGyLRuB0 - REACT_APP_MDN_PLUS_5M_PLAN: price_1JFoTYKb9q6OnNsLalexa03p - REACT_APP_MDN_PLUS_5Y_PLAN: price_1JpIPwKb9q6OnNsLJLsIqMp7 - REACT_APP_MDN_PLUS_10M_PLAN: price_1K6X7gKb9q6OnNsLi44HdLcC - REACT_APP_MDN_PLUS_10Y_PLAN: price_1K6X8VKb9q6OnNsLFlUcEiu4 - - # Surveys. - REACT_APP_SURVEY_START_CONTENT_DISCOVERY_2023: 0 # stage - REACT_APP_SURVEY_END_CONTENT_DISCOVERY_2023: 1677672000000 # (new Date("2023-03-01 12:00:00Z")).getTime() - REACT_APP_SURVEY_RATE_FROM_CONTENT_DISCOVERY_2023: 0.0 - REACT_APP_SURVEY_RATE_TILL_CONTENT_DISCOVERY_2023: 0.05 # 5% - - # Telemetry. - REACT_APP_GLEAN_CHANNEL: xyz - REACT_APP_GLEAN_ENABLED: true - - # Newsletter - REACT_APP_NEWSLETTER_ENABLED: false - - # Placement - REACT_APP_PLACEMENT_ENABLED: true - - run: | - - # Info about which CONTENT_* environment variables were set and to what. - echo "CONTENT_ROOT=$CONTENT_ROOT" - echo "CONTENT_TRANSLATED_ROOT=$CONTENT_TRANSLATED_ROOT" - # Build the ServiceWorker first - yarn build:sw - yarn build:prepare - - # (July 15, 2021) This is a temporary solution. This should become an - # integrated part of 'build:prepare'. - # See https://github.com/mdn/yari/issues/4217 - yarn tool popularities - - yarn build --locale en-us - - du -sh client/build - - # Generate sitemap index file - yarn build --sitemap-index - - name: Authenticate with GCP uses: google-github-actions/auth@v0 with: @@ -155,9 +74,6 @@ jobs: - name: Setup gcloud uses: google-github-actions/setup-gcloud@v1 - - name: Sync build with GCS bucket - run: gsutil -q -m rsync -d -r client/build/ gs://${{ secrets.GCS_BUCKET }}/ - - name: Deploy Function env: CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files From 6f7a97a2e6e76e2f9f8f90ccfaf5a6119870b66d Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 23 Mar 2023 22:24:34 +0100 Subject: [PATCH 109/343] ci(xyz-build): use google-github-actions/deploy-cloud-functions --- .github/workflows/xyz-build.yml | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index b965e75b67d1..42eba42e02c4 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -74,12 +74,25 @@ jobs: - name: Setup gcloud uses: google-github-actions/setup-gcloud@v1 - - name: Deploy Function + - name: Build Function + working-directory: gcp/function env: CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files - working-directory: gcp/function run: | npm ci npm run build - npm run deploy + + - name: Deploy Function + uses: google-github-actions/deploy-cloud-functions@v1 + working-directory: gcp/function + with: + name: mdn-two + runtime: nodejs18 + memory_mb: 256 + region: europe-west3 + env_vars: |- + ORIGIN_MAIN=developer.allizom.xyz + ORIGIN_LIVE_SAMPLES=yari-demos.developer.allizom.xyz + SOURCE_CONTENT=https://storage.googleapis.com/fiji-mdn-content/ + SOURCE_RUMBA=https://developer.allizom.org/ \ No newline at end of file From f5871de70dce0a45ef6c0859f3f5085795b69573 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 23 Mar 2023 22:27:18 +0100 Subject: [PATCH 110/343] fixup! ci(xyz-build): use google-github-actions/deploy-cloud-functions --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 42eba42e02c4..7eafb9d33f97 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -85,12 +85,12 @@ jobs: - name: Deploy Function uses: google-github-actions/deploy-cloud-functions@v1 - working-directory: gcp/function with: name: mdn-two runtime: nodejs18 memory_mb: 256 region: europe-west3 + source_dir: gcp/function env_vars: |- ORIGIN_MAIN=developer.allizom.xyz ORIGIN_LIVE_SAMPLES=yari-demos.developer.allizom.xyz From 4abfa6c2670a2349d0160160a5de744b671ea68f Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 23 Mar 2023 22:27:44 +0100 Subject: [PATCH 111/343] ci(xyz-build): allow concurrency --- .github/workflows/xyz-build.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 7eafb9d33f97..8fd2a6edca35 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -16,8 +16,6 @@ on: jobs: build: environment: xyz - concurrency: - group: ${{ github.workflow }} permissions: contents: read id-token: write From 60e4d8ec131477b714db9de92e7c8ef8d0c510de Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 23 Mar 2023 22:34:09 +0100 Subject: [PATCH 112/343] fix(gcp/function): set secret_environment_variables --- .github/workflows/xyz-build.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 8fd2a6edca35..fd2a61b9f24a 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -93,4 +93,10 @@ jobs: ORIGIN_MAIN=developer.allizom.xyz ORIGIN_LIVE_SAMPLES=yari-demos.developer.allizom.xyz SOURCE_CONTENT=https://storage.googleapis.com/fiji-mdn-content/ - SOURCE_RUMBA=https://developer.allizom.org/ \ No newline at end of file + SOURCE_RUMBA=https://developer.allizom.org/ + secret_environment_variables: |- + KEVEL_SITE_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-site-id:latest + KEVEL_NETWORK_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-network-id:latest + SIGN_SECRET=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-sign-secret:latest + CARBON_ZONE_KEY=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-carbon-zone-key:latest + CARBON_FALLBACK_ENABLED=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-fallback-enabled:latest From adc3e2e6cd1e7d8130114cd2e02383b4aebef356 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 23 Mar 2023 22:41:48 +0100 Subject: [PATCH 113/343] fix(gcp/function): set entry_point --- .github/workflows/xyz-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index fd2a61b9f24a..8e253d38776a 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -86,6 +86,7 @@ jobs: with: name: mdn-two runtime: nodejs18 + entry_point: mdnFunction memory_mb: 256 region: europe-west3 source_dir: gcp/function From 9c81e2dd9e8c5d9d1658ac13c597118a1ee3cf89 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 23 Mar 2023 22:45:35 +0100 Subject: [PATCH 114/343] fixup! fix(gcp/function): set entry_point --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 8e253d38776a..d45c2da6cd0f 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -86,7 +86,7 @@ jobs: with: name: mdn-two runtime: nodejs18 - entry_point: mdnFunction + entry_point: mdnHandler memory_mb: 256 region: europe-west3 source_dir: gcp/function From aeaedbffabcfd52a42f35b36e24282821afdae7f Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 23 Mar 2023 22:47:16 +0100 Subject: [PATCH 115/343] fixup! fix(gcp/function): set secret_environment_variables --- .github/workflows/xyz-build.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index d45c2da6cd0f..9d578a42bd2e 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -96,8 +96,8 @@ jobs: SOURCE_CONTENT=https://storage.googleapis.com/fiji-mdn-content/ SOURCE_RUMBA=https://developer.allizom.org/ secret_environment_variables: |- - KEVEL_SITE_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-site-id:latest - KEVEL_NETWORK_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-network-id:latest - SIGN_SECRET=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-sign-secret:latest - CARBON_ZONE_KEY=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-carbon-zone-key:latest - CARBON_FALLBACK_ENABLED=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-fallback-enabled:latest + KEVEL_SITE_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-site-id/versions/latest + KEVEL_NETWORK_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-network-id/versions/latest + SIGN_SECRET=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-sign-secret/versions/latest + CARBON_ZONE_KEY=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-carbon-zone-key/versions/latest + CARBON_FALLBACK_ENABLED=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-fallback-enabled/versions/latest From d690137161f1c9f95448354a50233cf181f37258 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 23 Mar 2023 22:54:25 +0100 Subject: [PATCH 116/343] Revert "ci(xyz-build): allow concurrency" This reverts commit 4abfa6c2670a2349d0160160a5de744b671ea68f. --- .github/workflows/xyz-build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 9d578a42bd2e..5e00f93e66fe 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -16,6 +16,8 @@ on: jobs: build: environment: xyz + concurrency: + group: ${{ github.workflow }} permissions: contents: read id-token: write From 1765d34781d3096a4760dbf7af27f9df65e0cb01 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 23 Mar 2023 22:55:13 +0100 Subject: [PATCH 117/343] Revert "ci(xyz-build): remove build temporarily" This reverts commit 58b57c5994ab81e10e676f52433c8504b8e54486. --- .github/workflows/xyz-build.yml | 84 +++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 5e00f93e66fe..6c391ab87674 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -64,6 +64,87 @@ jobs: - name: Print information about CPU run: cat /proc/cpuinfo + - name: Build everything + env: + # Remember, the mdn/content repo got cloned into `pwd` into a + # sub-folder called "mdn/content" + CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files + CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files + CONTRIBUTOR_SPOTLIGHT_ROOT: ${{ github.workspace }}/mdn/mdn-contributor-spotlight/contributors + + # The default for this environment variable is geared for writers + # (aka. local development). Usually defaults are supposed to be for + # secure production but this is an exception and default + # is not insecure. + BUILD_LIVE_SAMPLES_BASE_URL: https://yari-demos.developer.allizom.xyz + + # Use the stage version of interactive examples. + BUILD_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net + + # Now is not the time to worry about flaws. + BUILD_FLAW_LEVELS: "*:ignore" + + # This is the Google Analytics account ID for developer.mozilla.org + # If it's used on other domains (e.g. stage or dev builds), it's OK + # because ultimately Google Analytics will filter it out since the + # origin domain isn't what that account expects. + #BUILD_GOOGLE_ANALYTICS_ACCOUNT: UA-36116321-5 + + # This enables the Plus call-to-action banner and the Plus landing page + REACT_APP_ENABLE_PLUS: true + + # This adds the ability to sign in (stage only for now) + REACT_APP_DISABLE_AUTH: false + + # Use the stage version of interactive examples in react app + REACT_APP_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net + + # Firefox Accounts and SubPlat settings + REACT_APP_FXA_SIGNIN_URL: /users/fxa/login/authenticate/ + REACT_APP_FXA_SETTINGS_URL: https://accounts.stage.mozaws.net/settings/ + REACT_APP_MDN_PLUS_SUBSCRIBE_URL: https://accounts.stage.mozaws.net/subscriptions/products/prod_Jtbg9tyGyLRuB0 + REACT_APP_MDN_PLUS_5M_PLAN: price_1JFoTYKb9q6OnNsLalexa03p + REACT_APP_MDN_PLUS_5Y_PLAN: price_1JpIPwKb9q6OnNsLJLsIqMp7 + REACT_APP_MDN_PLUS_10M_PLAN: price_1K6X7gKb9q6OnNsLi44HdLcC + REACT_APP_MDN_PLUS_10Y_PLAN: price_1K6X8VKb9q6OnNsLFlUcEiu4 + + # Surveys. + REACT_APP_SURVEY_START_CONTENT_DISCOVERY_2023: 0 # stage + REACT_APP_SURVEY_END_CONTENT_DISCOVERY_2023: 1677672000000 # (new Date("2023-03-01 12:00:00Z")).getTime() + REACT_APP_SURVEY_RATE_FROM_CONTENT_DISCOVERY_2023: 0.0 + REACT_APP_SURVEY_RATE_TILL_CONTENT_DISCOVERY_2023: 0.05 # 5% + + # Telemetry. + REACT_APP_GLEAN_CHANNEL: xyz + REACT_APP_GLEAN_ENABLED: true + + # Newsletter + REACT_APP_NEWSLETTER_ENABLED: false + + # Placement + REACT_APP_PLACEMENT_ENABLED: true + + run: | + + # Info about which CONTENT_* environment variables were set and to what. + echo "CONTENT_ROOT=$CONTENT_ROOT" + echo "CONTENT_TRANSLATED_ROOT=$CONTENT_TRANSLATED_ROOT" + # Build the ServiceWorker first + yarn build:sw + yarn build:prepare + + # (July 15, 2021) This is a temporary solution. This should become an + # integrated part of 'build:prepare'. + # See https://github.com/mdn/yari/issues/4217 + yarn tool popularities + + yarn build --locale en-us + + du -sh client/build + + # Generate sitemap index file + yarn build --sitemap-index + - name: Authenticate with GCP uses: google-github-actions/auth@v0 with: @@ -74,6 +155,9 @@ jobs: - name: Setup gcloud uses: google-github-actions/setup-gcloud@v1 + - name: Sync build with GCS bucket + run: gsutil -q -m rsync -d -r client/build/ gs://${{ secrets.GCS_BUCKET }}/ + - name: Build Function working-directory: gcp/function env: From c9c92ac98ac6b584a5cd76bb3030783edc1d9c5b Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 23 Mar 2023 23:20:04 +0100 Subject: [PATCH 118/343] fix(gcp/function): apply req.originalUrl workaround to search-index.json route --- gcp/function/src/app.ts | 2 +- gcp/function/src/middlewares/pathnameLC.ts | 3 +++ gcp/function/src/middlewares/resolveIndexHTML.ts | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 93c4ca8c0cd1..2180eb3ede84 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -34,7 +34,7 @@ mainRouter.get( resolveIndexHTML, proxyContent ); -mainRouter.get("/[^/]+/search-index.json", proxyContent); +mainRouter.get("/[^/]+/search-index.json", pathnameLC, proxyContent); mainRouter.get( "*", redirectFundamental, diff --git a/gcp/function/src/middlewares/pathnameLC.ts b/gcp/function/src/middlewares/pathnameLC.ts index ac3c6b625cd9..9ac82839d4b7 100644 --- a/gcp/function/src/middlewares/pathnameLC.ts +++ b/gcp/function/src/middlewares/pathnameLC.ts @@ -12,5 +12,8 @@ export function pathnameLC( urlParsed.pathname = urlParsed.pathname.toLowerCase(); } req.url = url.format(urlParsed); + // 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/gcp/function/src/middlewares/resolveIndexHTML.ts b/gcp/function/src/middlewares/resolveIndexHTML.ts index 2f59b0763083..35d2c39e6ec3 100644 --- a/gcp/function/src/middlewares/resolveIndexHTML.ts +++ b/gcp/function/src/middlewares/resolveIndexHTML.ts @@ -11,7 +11,7 @@ export function resolveIndexHTML( if (path.extname(resolvedUrl) === "") { resolvedUrl = path.join(resolvedUrl, "index.html"); } - req.originalUrl = req.url = resolvedUrl; + req.url = resolvedUrl; // Workaround for http-proxy-middleware v2 using `req.originalUrl`. // See: https://github.com/chimurai/http-proxy-middleware/pull/731 req.originalUrl = req.url; From 4593a0a8a9e1241a566f72e4212bb694022d1576 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 24 Mar 2023 15:52:34 +0100 Subject: [PATCH 119/343] chore(gcp/function): add X-Forwarded headers --- gcp/function/src/handlers/content.ts | 1 + gcp/function/src/handlers/rumba.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/gcp/function/src/handlers/content.ts b/gcp/function/src/handlers/content.ts index 5fff1c65e025..cb36a19b1408 100644 --- a/gcp/function/src/handlers/content.ts +++ b/gcp/function/src/handlers/content.ts @@ -16,6 +16,7 @@ export function createContentProxy(): express.Handler { changeOrigin: true, target, autoRewrite: true, + xfwd: true, selfHandleResponse: true, onProxyRes: responseInterceptor( async (responseBuffer, proxyRes, req, res) => { diff --git a/gcp/function/src/handlers/rumba.ts b/gcp/function/src/handlers/rumba.ts index fdc444ff1bf3..fa2a37c51496 100644 --- a/gcp/function/src/handlers/rumba.ts +++ b/gcp/function/src/handlers/rumba.ts @@ -6,4 +6,5 @@ export const proxyRumba = createProxyMiddleware({ target: sourceUri(Source.rumba), changeOrigin: true, autoRewrite: true, + xfwd: true, }); From 2f149c834f87982fa78ca045b1b0590f8928e87b Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 27 Mar 2023 17:00:58 +0200 Subject: [PATCH 120/343] fix(gcp/function): don't lowercase /static/ --- gcp/function/src/app.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 2180eb3ede84..ba82a8b14ee7 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -25,6 +25,7 @@ mainRouter.all("/users/fxa/*", proxyRumba); mainRouter.all("/submit/mdn-yari/*", proxyTelemetry); mainRouter.all("/pong/*", express.json(), proxyKevel); mainRouter.all("/pimg/*", proxyKevel); +mainRouter.get("/static/*", proxyContent); mainRouter.get( "/[^/]+/docs/*", redirectFundamental, From 65393dbe674b1f22d0640c86ba7408bc990a53d0 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 27 Mar 2023 19:45:38 +0200 Subject: [PATCH 121/343] fix(gcp/function): separate sitemaps + add content-encoding --- gcp/function/src/app.ts | 1 + gcp/function/src/headers.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index ba82a8b14ee7..eca70da2ba31 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -25,6 +25,7 @@ mainRouter.all("/users/fxa/*", proxyRumba); mainRouter.all("/submit/mdn-yari/*", proxyTelemetry); mainRouter.all("/pong/*", express.json(), proxyKevel); mainRouter.all("/pimg/*", proxyKevel); +mainRouter.get("/sitemaps/*", proxyContent); mainRouter.get("/static/*", proxyContent); mainRouter.get( "/[^/]+/docs/*", diff --git a/gcp/function/src/headers.ts b/gcp/function/src/headers.ts index fcdb28090797..728bde1d615e 100644 --- a/gcp/function/src/headers.ts +++ b/gcp/function/src/headers.ts @@ -23,6 +23,11 @@ export function withProxyResponseHeaders( xFrame: !isLiveSampleURI, }); + if (req.url?.endsWith("/sitemap.xml.gz")) { + res.setHeader("Content-Type", "application/xml"); + res.setHeader("Content-Encoding", "gzip"); + } + return res; } From 5399c399d440b71017b81f1dbc6dd9b7390b06ab Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 30 Mar 2023 14:26:33 +0200 Subject: [PATCH 122/343] ci(xyz-build): use function name from vars context --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 6c391ab87674..d3c8cee10c3e 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -170,7 +170,7 @@ jobs: - name: Deploy Function uses: google-github-actions/deploy-cloud-functions@v1 with: - name: mdn-two + name: ${{ vars.GCP_FUNCTION_NAME }} runtime: nodejs18 entry_point: mdnHandler memory_mb: 256 From bc51fe4994d527e7b6bb04af5b737c5826f1051c Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 30 Mar 2023 14:26:55 +0200 Subject: [PATCH 123/343] chore(gcp/function): remove manual deployment --- gcp/function/package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/gcp/function/package.json b/gcp/function/package.json index b1ca3fb0df38..1a2fa1dac752 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -9,10 +9,8 @@ "main": "src/index.js", "scripts": { "build": "tsc -b && ts-node src/build.ts", - "build-deploy-clean": "npm-run-all -s build deploy clean", "clean": "tsc -b --clean && git checkout -- redirects.json", "copy-internal": "rm -rf ./src/internal && cp -R ../../libs ./src/internal", - "deploy": "gcloud functions deploy mdn-two --region europe-west3 --runtime nodejs18 --trigger-http --allow-unauthenticated", "package": "zip -r -X f.zip . -i package.json .env 'src/*.js' 'src/handlers/*.js' 'src/internal'", "prepare": "[ ! -e ../../libs ] || npm run copy-internal", "start": "ts-node src/cli.ts" From 2ca83db5ab25be772160057e10e4591e75bb5ba1 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 30 Mar 2023 14:31:50 +0200 Subject: [PATCH 124/343] ci(xyz-build): deploy into unique subfolder --- .github/workflows/xyz-build.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index d3c8cee10c3e..b7e64f0c879a 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -24,6 +24,9 @@ jobs: runs-on: ubuntu-latest + env: + DEPLOYMENT_ID: ${{ github.run_id }}-${{ github.run_attempt }} + # Only run the scheduled workflows on the main repo. if: github.repository == 'mdn/yari' @@ -156,7 +159,7 @@ jobs: uses: google-github-actions/setup-gcloud@v1 - name: Sync build with GCS bucket - run: gsutil -q -m rsync -d -r client/build/ gs://${{ secrets.GCS_BUCKET }}/ + run: gsutil -q -m cp -r client/build/ gs://${{ secrets.GCS_BUCKET }}/$DEPLOYMENT_ID/ - name: Build Function working-directory: gcp/function @@ -179,7 +182,7 @@ jobs: env_vars: |- ORIGIN_MAIN=developer.allizom.xyz ORIGIN_LIVE_SAMPLES=yari-demos.developer.allizom.xyz - SOURCE_CONTENT=https://storage.googleapis.com/fiji-mdn-content/ + SOURCE_CONTENT=https://storage.googleapis.com/fiji-mdn-content/$DEPLOYMENT_ID/ SOURCE_RUMBA=https://developer.allizom.org/ secret_environment_variables: |- KEVEL_SITE_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-site-id/versions/latest From 1474fde2979ccc4ef1c01f033e7aabca06d80c1f Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 30 Mar 2023 17:43:28 +0200 Subject: [PATCH 125/343] fixup! ci(xyz-build): deploy into unique subfolder --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index b7e64f0c879a..64ad2e0243eb 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -182,7 +182,7 @@ jobs: env_vars: |- ORIGIN_MAIN=developer.allizom.xyz ORIGIN_LIVE_SAMPLES=yari-demos.developer.allizom.xyz - SOURCE_CONTENT=https://storage.googleapis.com/fiji-mdn-content/$DEPLOYMENT_ID/ + SOURCE_CONTENT=https://storage.googleapis.com/fiji-mdn-content/${{ env.DEPLOYMENT_ID }}/ SOURCE_RUMBA=https://developer.allizom.org/ secret_environment_variables: |- KEVEL_SITE_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-site-id/versions/latest From d9da29262432722f06af9c9f3d3a00ae33c6baee Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 30 Mar 2023 18:39:13 +0200 Subject: [PATCH 126/343] fixup! fixup! ci(xyz-build): deploy into unique subfolder --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 64ad2e0243eb..b1db20d1a7fe 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -159,7 +159,7 @@ jobs: uses: google-github-actions/setup-gcloud@v1 - name: Sync build with GCS bucket - run: gsutil -q -m cp -r client/build/ gs://${{ secrets.GCS_BUCKET }}/$DEPLOYMENT_ID/ + run: gsutil -q -m cp -r client/build gs://${{ secrets.GCS_BUCKET }}/$DEPLOYMENT_ID - name: Build Function working-directory: gcp/function From 62168f199252fc5b7c5504c7734eb2c83dd0a39c Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 30 Mar 2023 18:55:39 +0200 Subject: [PATCH 127/343] chore(gcp): cleanup --- gcp.sh | 1 - libs/constants/index.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100755 gcp.sh diff --git a/gcp.sh b/gcp.sh deleted file mode 100755 index f53eaf7f7eb4..000000000000 --- a/gcp.sh +++ /dev/null @@ -1 +0,0 @@ -cd gcp/function && npm i && npm run build-deploy-clean diff --git a/libs/constants/index.js b/libs/constants/index.js index 3daa6499eaae..9d7c3268c0ed 100644 --- a/libs/constants/index.js +++ b/libs/constants/index.js @@ -117,8 +117,8 @@ export const CSP_DIRECTIVES = { "yari-demos.stage.mdn.mozit.cloud", // XYZ. - "interactive-examples.developer.allizom.xyz", "yari-demos.developer.allizom.xyz", + "mdn.github.io", "mdn.mozillademos.org", From 1b8f3df11ea8d65f2d0db5917ba54906c9333c92 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 30 Mar 2023 19:02:46 +0200 Subject: [PATCH 128/343] ci(xyz-build): build all locales --- .github/workflows/xyz-build.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index b1db20d1a7fe..a0a892ff0975 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -141,7 +141,21 @@ jobs: # See https://github.com/mdn/yari/issues/4217 yarn tool popularities - yarn build --locale en-us + yarn tool sync-translated-content + + # Spread the work across 2 processes. Why 2? Because that's what you + # get in the default GitHub hosting Linux runners. + # See https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources + yarn build --locale en-us --locale ja --locale fr & + build1=$! + yarn build --not-locale en-us --not-locale ja --not-locale fr & + build2=$! + + # You must explicitly specify the job you're waiting-on to ensure + # that the exit status of the wait command reflects the exit status + # of the job it's waiting-on. + wait $build1 + wait $build2 du -sh client/build From 8621e41809a52b23e4c339884b91319ca1f99df8 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 30 Mar 2023 19:06:37 +0200 Subject: [PATCH 129/343] ci(xyz-build): use more vars --- .github/workflows/xyz-build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index a0a892ff0975..0187473912af 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -173,7 +173,7 @@ jobs: uses: google-github-actions/setup-gcloud@v1 - name: Sync build with GCS bucket - run: gsutil -q -m cp -r client/build gs://${{ secrets.GCS_BUCKET }}/$DEPLOYMENT_ID + run: gsutil -q -m cp -r client/build gs://${{ vars.GCP_BUCKET_NAME }}/$DEPLOYMENT_ID - name: Build Function working-directory: gcp/function @@ -194,9 +194,9 @@ jobs: region: europe-west3 source_dir: gcp/function env_vars: |- - ORIGIN_MAIN=developer.allizom.xyz - ORIGIN_LIVE_SAMPLES=yari-demos.developer.allizom.xyz - SOURCE_CONTENT=https://storage.googleapis.com/fiji-mdn-content/${{ env.DEPLOYMENT_ID }}/ + ORIGIN_MAIN=${{ vars.ORIGIN_MAIN }} + ORIGIN_LIVE_SAMPLES=${{ vars.ORIGIN_LIVE_SAMPLES }} + SOURCE_CONTENT=https://storage.googleapis.com/${{ vars.GCP_BUCKET_NAME }}/${{ env.DEPLOYMENT_ID }}/ SOURCE_RUMBA=https://developer.allizom.org/ secret_environment_variables: |- KEVEL_SITE_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-site-id/versions/latest From 08399006a75fc80c602b51e1054adedbe44b315b Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 30 Mar 2023 23:54:03 +0200 Subject: [PATCH 130/343] ci(xyz-build): use large runner --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 0187473912af..d90c8bbe7520 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -22,7 +22,7 @@ jobs: contents: read id-token: write - runs-on: ubuntu-latest + runs-on: ubuntu-latest-large env: DEPLOYMENT_ID: ${{ github.run_id }}-${{ github.run_attempt }} From cee095b25ae68a6651d3533c33aa447af2ae3c06 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 31 Mar 2023 00:19:11 +0200 Subject: [PATCH 131/343] ci(xyz-build): build all locales in parallel --- .github/workflows/xyz-build.yml | 36 +++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index d90c8bbe7520..4d3565d49a29 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -143,19 +143,39 @@ jobs: yarn tool sync-translated-content - # Spread the work across 2 processes. Why 2? Because that's what you - # get in the default GitHub hosting Linux runners. - # See https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources - yarn build --locale en-us --locale ja --locale fr & + yarn build --locale en-us > en-us.log & build1=$! - yarn build --not-locale en-us --not-locale ja --not-locale fr & + yarn build --locale es > es.log & build2=$! + yarn build --locale fr > fr.log & + build3=$! + yarn build --locale ja > ja.log & + build4=$! + yarn build --locale ko > ko.log & + build5=$! + yarn build --locale pt-br > pt-br.log & + build6=$! + yarn build --locale ru > ru.log & + build7=$! + yarn build --locale zh-cn > zh-cn.log & + build8=$! + yarn build --locale zh-tw > zh-tw.log & + build9=$! + + tail -n 0 -F *.log & + tail=$! - # You must explicitly specify the job you're waiting-on to ensure - # that the exit status of the wait command reflects the exit status - # of the job it's waiting-on. wait $build1 wait $build2 + wait $build3 + wait $build4 + wait $build5 + wait $build6 + wait $build7 + wait $build8 + wait $build9 + + kill $tail du -sh client/build From 78dcb358600202ac4ca5f181e71858a6e6277776 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 31 Mar 2023 00:20:05 +0200 Subject: [PATCH 132/343] ci(xyz-build): allow concurrency --- .github/workflows/xyz-build.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 4d3565d49a29..46b7c0933d9f 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -16,8 +16,6 @@ on: jobs: build: environment: xyz - concurrency: - group: ${{ github.workflow }} permissions: contents: read id-token: write From 82ad81cde51278ea585f1ef0c2cf75d16e8619a3 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 31 Mar 2023 01:15:02 +0200 Subject: [PATCH 133/343] ci(xyz-build): omit tail header --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 46b7c0933d9f..af063a23579d 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -160,7 +160,7 @@ jobs: yarn build --locale zh-tw > zh-tw.log & build9=$! - tail -n 0 -F *.log & + tail -n 0 -qF *.log & tail=$! wait $build1 From c0dcc668bd37673ada4065842e07a2176c465f79 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 31 Mar 2023 01:18:06 +0200 Subject: [PATCH 134/343] ci(xyz-build): loop over locales + pids --- .github/workflows/xyz-build.yml | 34 +++++++-------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index af063a23579d..328c97ff3387 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -141,37 +141,17 @@ jobs: yarn tool sync-translated-content - yarn build --locale en-us > en-us.log & - build1=$! - yarn build --locale es > es.log & - build2=$! - yarn build --locale fr > fr.log & - build3=$! - yarn build --locale ja > ja.log & - build4=$! - yarn build --locale ko > ko.log & - build5=$! - yarn build --locale pt-br > pt-br.log & - build6=$! - yarn build --locale ru > ru.log & - build7=$! - yarn build --locale zh-cn > zh-cn.log & - build8=$! - yarn build --locale zh-tw > zh-tw.log & - build9=$! + for locale in en-us es fr ja ko pt-br ru zh-cn zh-tw; do + yarn build --locale $locale & + pids+=($!) + done tail -n 0 -qF *.log & tail=$! - wait $build1 - wait $build2 - wait $build3 - wait $build4 - wait $build5 - wait $build6 - wait $build7 - wait $build8 - wait $build9 + for pid in "${pids[@]}"; do + wait $pid + done kill $tail From ba8da5f7b9e7745bf694746e423c057e82febcb5 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 31 Mar 2023 14:25:26 +0200 Subject: [PATCH 135/343] ci(xyz-build): switch back to rsync with -c Implies deploying to the same path. --- .github/workflows/xyz-build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 328c97ff3387..86860df0d3e8 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest-large env: - DEPLOYMENT_ID: ${{ github.run_id }}-${{ github.run_attempt }} + BUCKET_PATH: main # Only run the scheduled workflows on the main repo. if: github.repository == 'mdn/yari' @@ -171,7 +171,7 @@ jobs: uses: google-github-actions/setup-gcloud@v1 - name: Sync build with GCS bucket - run: gsutil -q -m cp -r client/build gs://${{ vars.GCP_BUCKET_NAME }}/$DEPLOYMENT_ID + run: gsutil -q -m rsync -cdr client/build gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH - name: Build Function working-directory: gcp/function @@ -194,7 +194,7 @@ jobs: env_vars: |- ORIGIN_MAIN=${{ vars.ORIGIN_MAIN }} ORIGIN_LIVE_SAMPLES=${{ vars.ORIGIN_LIVE_SAMPLES }} - SOURCE_CONTENT=https://storage.googleapis.com/${{ vars.GCP_BUCKET_NAME }}/${{ env.DEPLOYMENT_ID }}/ + SOURCE_CONTENT=https://storage.googleapis.com/${{ vars.GCP_BUCKET_NAME }}/${{ env.BUCKET_PATH }}/ SOURCE_RUMBA=https://developer.allizom.org/ secret_environment_variables: |- KEVEL_SITE_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-site-id/versions/latest From ec7a8640904139919e4db2df93b943570eb94145 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 31 Mar 2023 14:27:15 +0200 Subject: [PATCH 136/343] ci(xyz-build): copy static first --- .github/workflows/xyz-build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 86860df0d3e8..49f75b81ea0b 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -171,7 +171,9 @@ jobs: uses: google-github-actions/setup-gcloud@v1 - name: Sync build with GCS bucket - run: gsutil -q -m rsync -cdr client/build gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH + run: | + gsutil -q -m cp -r client/build/static gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH/static + gsutil -q -m rsync -cdr client/build gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH - name: Build Function working-directory: gcp/function From 483d483f39b7ffbb6bc19765a79ed37b73815657 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 31 Mar 2023 15:10:42 +0200 Subject: [PATCH 137/343] ci(xyz-build): use runner with 4 cores --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 49f75b81ea0b..18441d548a67 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -20,7 +20,7 @@ jobs: contents: read id-token: write - runs-on: ubuntu-latest-large + runs-on: ubuntu-latest-4core env: BUCKET_PATH: main From aac8297984ea659a02f35a8e6079b48776980c7b Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 31 Mar 2023 21:41:38 +0200 Subject: [PATCH 138/343] ci(xyz-build): rsync with -j html,json,txt --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 18441d548a67..3db238b91b6f 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -173,7 +173,7 @@ jobs: - name: Sync build with GCS bucket run: | gsutil -q -m cp -r client/build/static gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH/static - gsutil -q -m rsync -cdr client/build gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH + gsutil -q -m rsync -cdrj html,json,txt client/build gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH - name: Build Function working-directory: gcp/function From 4c4b5a2e45a2bd0e2174c2bdfecabc7987bd7290 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 31 Mar 2023 21:59:05 +0200 Subject: [PATCH 139/343] ci(xyz-build): temporarily remove build/sync --- .github/workflows/xyz-build.yml | 100 -------------------------------- 1 file changed, 100 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 3db238b91b6f..946fafe46681 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -65,101 +65,6 @@ jobs: - name: Print information about CPU run: cat /proc/cpuinfo - - name: Build everything - env: - # Remember, the mdn/content repo got cloned into `pwd` into a - # sub-folder called "mdn/content" - CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files - CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files - CONTRIBUTOR_SPOTLIGHT_ROOT: ${{ github.workspace }}/mdn/mdn-contributor-spotlight/contributors - - # The default for this environment variable is geared for writers - # (aka. local development). Usually defaults are supposed to be for - # secure production but this is an exception and default - # is not insecure. - BUILD_LIVE_SAMPLES_BASE_URL: https://yari-demos.developer.allizom.xyz - - # Use the stage version of interactive examples. - BUILD_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net - - # Now is not the time to worry about flaws. - BUILD_FLAW_LEVELS: "*:ignore" - - # This is the Google Analytics account ID for developer.mozilla.org - # If it's used on other domains (e.g. stage or dev builds), it's OK - # because ultimately Google Analytics will filter it out since the - # origin domain isn't what that account expects. - #BUILD_GOOGLE_ANALYTICS_ACCOUNT: UA-36116321-5 - - # This enables the Plus call-to-action banner and the Plus landing page - REACT_APP_ENABLE_PLUS: true - - # This adds the ability to sign in (stage only for now) - REACT_APP_DISABLE_AUTH: false - - # Use the stage version of interactive examples in react app - REACT_APP_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net - - # Firefox Accounts and SubPlat settings - REACT_APP_FXA_SIGNIN_URL: /users/fxa/login/authenticate/ - REACT_APP_FXA_SETTINGS_URL: https://accounts.stage.mozaws.net/settings/ - REACT_APP_MDN_PLUS_SUBSCRIBE_URL: https://accounts.stage.mozaws.net/subscriptions/products/prod_Jtbg9tyGyLRuB0 - REACT_APP_MDN_PLUS_5M_PLAN: price_1JFoTYKb9q6OnNsLalexa03p - REACT_APP_MDN_PLUS_5Y_PLAN: price_1JpIPwKb9q6OnNsLJLsIqMp7 - REACT_APP_MDN_PLUS_10M_PLAN: price_1K6X7gKb9q6OnNsLi44HdLcC - REACT_APP_MDN_PLUS_10Y_PLAN: price_1K6X8VKb9q6OnNsLFlUcEiu4 - - # Surveys. - REACT_APP_SURVEY_START_CONTENT_DISCOVERY_2023: 0 # stage - REACT_APP_SURVEY_END_CONTENT_DISCOVERY_2023: 1677672000000 # (new Date("2023-03-01 12:00:00Z")).getTime() - REACT_APP_SURVEY_RATE_FROM_CONTENT_DISCOVERY_2023: 0.0 - REACT_APP_SURVEY_RATE_TILL_CONTENT_DISCOVERY_2023: 0.05 # 5% - - # Telemetry. - REACT_APP_GLEAN_CHANNEL: xyz - REACT_APP_GLEAN_ENABLED: true - - # Newsletter - REACT_APP_NEWSLETTER_ENABLED: false - - # Placement - REACT_APP_PLACEMENT_ENABLED: true - - run: | - - # Info about which CONTENT_* environment variables were set and to what. - echo "CONTENT_ROOT=$CONTENT_ROOT" - echo "CONTENT_TRANSLATED_ROOT=$CONTENT_TRANSLATED_ROOT" - # Build the ServiceWorker first - yarn build:sw - yarn build:prepare - - # (July 15, 2021) This is a temporary solution. This should become an - # integrated part of 'build:prepare'. - # See https://github.com/mdn/yari/issues/4217 - yarn tool popularities - - yarn tool sync-translated-content - - for locale in en-us es fr ja ko pt-br ru zh-cn zh-tw; do - yarn build --locale $locale & - pids+=($!) - done - - tail -n 0 -qF *.log & - tail=$! - - for pid in "${pids[@]}"; do - wait $pid - done - - kill $tail - - du -sh client/build - - # Generate sitemap index file - yarn build --sitemap-index - - name: Authenticate with GCP uses: google-github-actions/auth@v0 with: @@ -170,11 +75,6 @@ jobs: - name: Setup gcloud uses: google-github-actions/setup-gcloud@v1 - - name: Sync build with GCS bucket - run: | - gsutil -q -m cp -r client/build/static gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH/static - gsutil -q -m rsync -cdrj html,json,txt client/build gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH - - name: Build Function working-directory: gcp/function env: From fb6d14e8439a1a23c11ccfdb7b0a54e32da2e5a7 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 31 Mar 2023 21:59:43 +0200 Subject: [PATCH 140/343] ci(xyz-build): deploy v2 function via gcloud CLI --- .github/workflows/xyz-build.yml | 42 ++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 946fafe46681..e0848e127261 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -85,22 +85,26 @@ jobs: npm run build - name: Deploy Function - uses: google-github-actions/deploy-cloud-functions@v1 - with: - name: ${{ vars.GCP_FUNCTION_NAME }} - runtime: nodejs18 - entry_point: mdnHandler - memory_mb: 256 - region: europe-west3 - source_dir: gcp/function - env_vars: |- - ORIGIN_MAIN=${{ vars.ORIGIN_MAIN }} - ORIGIN_LIVE_SAMPLES=${{ vars.ORIGIN_LIVE_SAMPLES }} - SOURCE_CONTENT=https://storage.googleapis.com/${{ vars.GCP_BUCKET_NAME }}/${{ env.BUCKET_PATH }}/ - SOURCE_RUMBA=https://developer.allizom.org/ - secret_environment_variables: |- - KEVEL_SITE_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-site-id/versions/latest - KEVEL_NETWORK_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-network-id/versions/latest - SIGN_SECRET=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-sign-secret/versions/latest - CARBON_ZONE_KEY=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-carbon-zone-key/versions/latest - CARBON_FALLBACK_ENABLED=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-fallback-enabled/versions/latest + run: |- + for region in europe-west1 us-west1 asia-east1; do + gcloud functions deploy mdn-xyz-$region \ + --gen2 \ + --runtime=nodejs18 \ + --region=$region \ + --source=gcp/function \ + --trigger-http \ + --allow-unauthenticated \ + --entry-point=mdnHandler \ + --memory=256MB \ + --timeout=60s \ + --set-env-vars="ORIGIN_MAIN=${{ vars.ORIGIN_MAIN }}" \ + --set-env-vars="ORIGIN_LIVE_SAMPLES=${{ vars.ORIGIN_LIVE_SAMPLES }}" \ + --set-env-vars="SOURCE_CONTENT=https://storage.googleapis.com/${{ vars.GCP_BUCKET_NAME }}/${{ env.BUCKET_PATH }}/" \ + --set-env-vars="SOURCE_RUMBA=https://developer.allizom.org/" \ + --set-secrets="KEVEL_SITE_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-site-id/versions/latest" \ + --set-secrets="KEVEL_NETWORK_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-network-id/versions/latest" \ + --set-secrets="SIGN_SECRET=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-sign-secret/versions/latest" \ + --set-secrets="CARBON_ZONE_KEY=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-carbon-zone-key/versions/latest" \ + --set-secrets="CARBON_FALLBACK_ENABLED=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-fallback-enabled/versions/latest" + \ + done From 8cce715f78bd164f295661212cdc0d5003c8ce3c Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 5 Apr 2023 22:59:20 +0200 Subject: [PATCH 141/343] ci(xyz-build): allow manual execution --- .github/workflows/xyz-build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index e0848e127261..16cc19000747 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -5,6 +5,8 @@ on: branches: - gcp + workflow_dispatch: + workflow_call: secrets: GCP_PROJECT_NAME: From 163e2633e27aeba5937455fba8549eb87f802be5 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 5 Apr 2023 23:04:23 +0200 Subject: [PATCH 142/343] chore(gcp/function): use live-samples.mdn.mozilla.net --- gcp/function/src/env.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gcp/function/src/env.ts b/gcp/function/src/env.ts index e0d79ad5e674..b158d16e173c 100644 --- a/gcp/function/src/env.ts +++ b/gcp/function/src/env.ts @@ -22,7 +22,7 @@ export enum Source { export const ORIGIN_MAIN: string = process.env["ORIGIN_MAIN"] || "developer.mozilla.org"; export const ORIGIN_LIVE_SAMPLES: string = - process.env["ORIGIN_LIVE_SAMPLES"] || "yari-demos.prod.mdn.mozit.cloud"; + process.env["ORIGIN_LIVE_SAMPLES"] || "live-samples.mdn.mozilla.net"; export function origin(req: express.Request): Origin { switch (req.hostname) { @@ -38,8 +38,7 @@ export function origin(req: express.Request): Origin { export const SOURCE_CONTENT: string = process.env["SOURCE_CONTENT"] || "https://developer.mozilla.org"; export const SOURCE_LIVE_SAMPLES: string = - process.env["SOURCE_LIVE_SAMPLES"] || - "https://yari-demos.prod.mdn.mozit.cloud"; + process.env["SOURCE_LIVE_SAMPLES"] || "https://live-samples.mdn.mozilla.net"; export const SOURCE_RUMBA: string = process.env["SOURCE_RUMBA"] || "https://developer.mozilla.org"; From fdd27067149f87e55f2e1789b05d144ddde9c018 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 5 Apr 2023 23:17:52 +0200 Subject: [PATCH 143/343] feat(gcp/function): wrap with @sentry/serverless --- .github/workflows/xyz-build.yml | 2 + gcp/function/package-lock.json | 181 ++++++++++++++++++++++++++++++++ gcp/function/package.json | 1 + gcp/function/src/index.ts | 9 +- 4 files changed, 192 insertions(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 16cc19000747..b88b37fe2fa9 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -103,6 +103,8 @@ jobs: --set-env-vars="ORIGIN_LIVE_SAMPLES=${{ vars.ORIGIN_LIVE_SAMPLES }}" \ --set-env-vars="SOURCE_CONTENT=https://storage.googleapis.com/${{ vars.GCP_BUCKET_NAME }}/${{ env.BUCKET_PATH }}/" \ --set-env-vars="SOURCE_RUMBA=https://developer.allizom.org/" \ + --set-env-vars="SENTRY_DSN=${{ secrets.SENTRY_DSN_CLOUD_FUNCTION }}" \ + --set-env-vars="SENTRY_ENVIRONMENET=xyz" \ --set-secrets="KEVEL_SITE_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-site-id/versions/latest" \ --set-secrets="KEVEL_NETWORK_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-network-id/versions/latest" \ --set-secrets="SIGN_SECRET=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-sign-secret/versions/latest" \ diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index bdbe7341b426..2ec0baf95f14 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@adzerk/decision-sdk": "^1.0.0-beta.20", "@google-cloud/functions-framework": "^3.1.3", + "@sentry/serverless": "^7.47.0", "@yari-internal/constants": "file:src/internal/constants", "@yari-internal/fundamental-redirects": "file:src/internal/fundamental-redirects", "@yari-internal/locale-utils": "file:src/internal/locale-utils", @@ -206,6 +207,106 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@sentry-internal/tracing": { + "version": "7.47.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.47.0.tgz", + "integrity": "sha512-udpHnCzF8DQsWf0gQwd0XFGp6Y8MOiwnl8vGt2ohqZGS3m1+IxoRLXsSkD8qmvN6KKDnwbaAvYnK0z0L+AW95g==", + "dependencies": { + "@sentry/core": "7.47.0", + "@sentry/types": "7.47.0", + "@sentry/utils": "7.47.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/core": { + "version": "7.47.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.47.0.tgz", + "integrity": "sha512-EFhZhKdMu7wKmWYZwbgTi8FNZ7Fq+HdlXiZWNz51Bqe3pHmfAkdHtAEs0Buo0v623MKA0CA4EjXIazGUM34XTg==", + "dependencies": { + "@sentry/types": "7.47.0", + "@sentry/utils": "7.47.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/node": { + "version": "7.47.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.47.0.tgz", + "integrity": "sha512-LTg2r5EV9yh4GLYDF+ViSltR9LLj/pcvk8YhANJcMO3Fp//xh8njcdU0FC2yNthUREawYDzAsVzLyCYJfV0H1A==", + "dependencies": { + "@sentry-internal/tracing": "7.47.0", + "@sentry/core": "7.47.0", + "@sentry/types": "7.47.0", + "@sentry/utils": "7.47.0", + "cookie": "^0.4.1", + "https-proxy-agent": "^5.0.0", + "lru_map": "^0.3.3", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/node/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sentry/serverless": { + "version": "7.47.0", + "resolved": "https://registry.npmjs.org/@sentry/serverless/-/serverless-7.47.0.tgz", + "integrity": "sha512-ROt35Kp1JiR/h37Cw6Fjv/MI3grZCKQSeA5SXpuJObwtpdHI5OIYULivsvwB6HieJBKhS/k3Iplnf/E39808Lg==", + "dependencies": { + "@sentry/node": "7.47.0", + "@sentry/types": "7.47.0", + "@sentry/utils": "7.47.0", + "@types/aws-lambda": "^8.10.62", + "@types/express": "^4.17.14", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/serverless/node_modules/@types/express": { + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", + "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@sentry/types": { + "version": "7.47.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.47.0.tgz", + "integrity": "sha512-GxXocplN0j1+uczovHrfkykl9wvkamDtWxlPUQgyGlbLGZn+UH1Y79D4D58COaFWGEZdSNKr62gZAjfEYu9nQA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/utils": { + "version": "7.47.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.47.0.tgz", + "integrity": "sha512-A89SaOLp6XeZfByeYo2C8Ecye/YAtk/gENuyOUhQEdMulI6mZdjqtHAp7pTMVgkBc/YNARVuoa+kR/IdRrTPkQ==", + "dependencies": { + "@sentry/types": "7.47.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@swc/core": { "version": "1.3.38", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.38.tgz", @@ -422,6 +523,11 @@ "integrity": "sha512-S8oM29O6nnRC3/+rwYV7GBYIIgNIZ52PCxqBG7OuItq9oATnYWy8FfeLKwvq5F7pIYjeeBSCI7y+l+Z9UEQpVQ==", "dev": true }, + "node_modules/@types/aws-lambda": { + "version": "8.10.114", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.114.tgz", + "integrity": "sha512-M8WpEGfC9iQ6V2Ccq6nGIXoQgeVc6z0Ngk8yCOL5V/TYIxshvb0MWQYLFFTZDesL0zmsoBc4OBjG9DB/4rei6w==" + }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -556,6 +662,38 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/ajv": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", @@ -1375,6 +1513,39 @@ } } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1759,6 +1930,11 @@ "node": ">=8" } }, + "node_modules/lru_map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", + "integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -2624,6 +2800,11 @@ } } }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", diff --git a/gcp/function/package.json b/gcp/function/package.json index 1a2fa1dac752..312607f3c153 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -18,6 +18,7 @@ "dependencies": { "@adzerk/decision-sdk": "^1.0.0-beta.20", "@google-cloud/functions-framework": "^3.1.3", + "@sentry/serverless": "^7.47.0", "@yari-internal/constants": "file:src/internal/constants", "@yari-internal/fundamental-redirects": "file:src/internal/fundamental-redirects", "@yari-internal/locale-utils": "file:src/internal/locale-utils", diff --git a/gcp/function/src/index.ts b/gcp/function/src/index.ts index 3dc1e5d2aa21..47d87546555f 100644 --- a/gcp/function/src/index.ts +++ b/gcp/function/src/index.ts @@ -1,5 +1,12 @@ import { createHandler } from "./app.js"; import functions from "@google-cloud/functions-framework"; +import { GCPFunction } from "@sentry/serverless"; + +let handler = createHandler(); + +if (process.env["SENTRY_DSN"]) { + GCPFunction.init(); + handler = GCPFunction.wrapHttpFunction(handler); +} -const handler = createHandler(); functions.http("mdnHandler", handler); From dde166e61f11a22de9882fb306ac2fb146879acd Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 5 Apr 2023 23:26:43 +0200 Subject: [PATCH 144/343] ci(xyz-build): invalidate CDN once function is deployed --- .github/workflows/xyz-build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index b88b37fe2fa9..77b55cf09306 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -112,3 +112,7 @@ jobs: --set-secrets="CARBON_FALLBACK_ENABLED=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-fallback-enabled/versions/latest" \ done + + - name: Invalidate CDN + run: |- + gcloud compute url-maps invalidate-cdn-cache ${{ vars.GCP_LOAD_BALANCER_NAME }} --path "/" From 22388a2de16674d406efc9a187155678106b9c8b Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 5 Apr 2023 23:37:52 +0200 Subject: [PATCH 145/343] fixup! ci(xyz-build): invalidate CDN once function is deployed --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 77b55cf09306..0a1677e4c3b4 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -115,4 +115,4 @@ jobs: - name: Invalidate CDN run: |- - gcloud compute url-maps invalidate-cdn-cache ${{ vars.GCP_LOAD_BALANCER_NAME }} --path "/" + gcloud compute url-maps invalidate-cdn-cache ${{ vars.GCP_LOAD_BALANCER_NAME }} --path "/*" From de05a08dd539e432e58bc87d104be5c2059b9a74 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 5 Apr 2023 23:43:13 +0200 Subject: [PATCH 146/343] fixup! feat(gcp/function): wrap with @sentry/serverless --- .github/workflows/xyz-build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 0a1677e4c3b4..8e8dd7848bbe 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -104,7 +104,9 @@ jobs: --set-env-vars="SOURCE_CONTENT=https://storage.googleapis.com/${{ vars.GCP_BUCKET_NAME }}/${{ env.BUCKET_PATH }}/" \ --set-env-vars="SOURCE_RUMBA=https://developer.allizom.org/" \ --set-env-vars="SENTRY_DSN=${{ secrets.SENTRY_DSN_CLOUD_FUNCTION }}" \ - --set-env-vars="SENTRY_ENVIRONMENET=xyz" \ + --set-env-vars="SENTRY_ENVIRONMENT=xyz" \ + --set-env-vars="SENTRY_TRACES_SAMPLE_RATE=0.01" \ + --set-env-vars="SENTRY_RELEASE=${{ github.sha }}" \ --set-secrets="KEVEL_SITE_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-site-id/versions/latest" \ --set-secrets="KEVEL_NETWORK_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-network-id/versions/latest" \ --set-secrets="SIGN_SECRET=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-sign-secret/versions/latest" \ From 999fcacd4f69a8a1be6f057af20c1a476e0ed064 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 5 Apr 2023 23:55:21 +0200 Subject: [PATCH 147/343] fixup! feat(gcp/function): wrap with @sentry/serverless --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 8e8dd7848bbe..3ed606db0157 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -105,7 +105,7 @@ jobs: --set-env-vars="SOURCE_RUMBA=https://developer.allizom.org/" \ --set-env-vars="SENTRY_DSN=${{ secrets.SENTRY_DSN_CLOUD_FUNCTION }}" \ --set-env-vars="SENTRY_ENVIRONMENT=xyz" \ - --set-env-vars="SENTRY_TRACES_SAMPLE_RATE=0.01" \ + --set-env-vars="SENTRY_TRACES_SAMPLE_RATE=${{ vars.SENTRY_TRACES_SAMPLE_RATE }}" \ --set-env-vars="SENTRY_RELEASE=${{ github.sha }}" \ --set-secrets="KEVEL_SITE_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-site-id/versions/latest" \ --set-secrets="KEVEL_NETWORK_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-network-id/versions/latest" \ From 090abf30917fd7c5c2fa6e131ad58365f0c1a036 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 00:09:13 +0200 Subject: [PATCH 148/343] Revert "ci(xyz-build): temporarily remove build/sync" This reverts commit 4c4b5a2e45a2bd0e2174c2bdfecabc7987bd7290. --- .github/workflows/xyz-build.yml | 100 ++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 3ed606db0157..4560f0753584 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -67,6 +67,101 @@ jobs: - name: Print information about CPU run: cat /proc/cpuinfo + - name: Build everything + env: + # Remember, the mdn/content repo got cloned into `pwd` into a + # sub-folder called "mdn/content" + CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files + CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files + CONTRIBUTOR_SPOTLIGHT_ROOT: ${{ github.workspace }}/mdn/mdn-contributor-spotlight/contributors + + # The default for this environment variable is geared for writers + # (aka. local development). Usually defaults are supposed to be for + # secure production but this is an exception and default + # is not insecure. + BUILD_LIVE_SAMPLES_BASE_URL: https://yari-demos.developer.allizom.xyz + + # Use the stage version of interactive examples. + BUILD_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net + + # Now is not the time to worry about flaws. + BUILD_FLAW_LEVELS: "*:ignore" + + # This is the Google Analytics account ID for developer.mozilla.org + # If it's used on other domains (e.g. stage or dev builds), it's OK + # because ultimately Google Analytics will filter it out since the + # origin domain isn't what that account expects. + #BUILD_GOOGLE_ANALYTICS_ACCOUNT: UA-36116321-5 + + # This enables the Plus call-to-action banner and the Plus landing page + REACT_APP_ENABLE_PLUS: true + + # This adds the ability to sign in (stage only for now) + REACT_APP_DISABLE_AUTH: false + + # Use the stage version of interactive examples in react app + REACT_APP_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net + + # Firefox Accounts and SubPlat settings + REACT_APP_FXA_SIGNIN_URL: /users/fxa/login/authenticate/ + REACT_APP_FXA_SETTINGS_URL: https://accounts.stage.mozaws.net/settings/ + REACT_APP_MDN_PLUS_SUBSCRIBE_URL: https://accounts.stage.mozaws.net/subscriptions/products/prod_Jtbg9tyGyLRuB0 + REACT_APP_MDN_PLUS_5M_PLAN: price_1JFoTYKb9q6OnNsLalexa03p + REACT_APP_MDN_PLUS_5Y_PLAN: price_1JpIPwKb9q6OnNsLJLsIqMp7 + REACT_APP_MDN_PLUS_10M_PLAN: price_1K6X7gKb9q6OnNsLi44HdLcC + REACT_APP_MDN_PLUS_10Y_PLAN: price_1K6X8VKb9q6OnNsLFlUcEiu4 + + # Surveys. + REACT_APP_SURVEY_START_CONTENT_DISCOVERY_2023: 0 # stage + REACT_APP_SURVEY_END_CONTENT_DISCOVERY_2023: 1677672000000 # (new Date("2023-03-01 12:00:00Z")).getTime() + REACT_APP_SURVEY_RATE_FROM_CONTENT_DISCOVERY_2023: 0.0 + REACT_APP_SURVEY_RATE_TILL_CONTENT_DISCOVERY_2023: 0.05 # 5% + + # Telemetry. + REACT_APP_GLEAN_CHANNEL: xyz + REACT_APP_GLEAN_ENABLED: true + + # Newsletter + REACT_APP_NEWSLETTER_ENABLED: false + + # Placement + REACT_APP_PLACEMENT_ENABLED: true + + run: | + + # Info about which CONTENT_* environment variables were set and to what. + echo "CONTENT_ROOT=$CONTENT_ROOT" + echo "CONTENT_TRANSLATED_ROOT=$CONTENT_TRANSLATED_ROOT" + # Build the ServiceWorker first + yarn build:sw + yarn build:prepare + + # (July 15, 2021) This is a temporary solution. This should become an + # integrated part of 'build:prepare'. + # See https://github.com/mdn/yari/issues/4217 + yarn tool popularities + + yarn tool sync-translated-content + + for locale in en-us es fr ja ko pt-br ru zh-cn zh-tw; do + yarn build --locale $locale & + pids+=($!) + done + + tail -n 0 -qF *.log & + tail=$! + + for pid in "${pids[@]}"; do + wait $pid + done + + kill $tail + + du -sh client/build + + # Generate sitemap index file + yarn build --sitemap-index + - name: Authenticate with GCP uses: google-github-actions/auth@v0 with: @@ -77,6 +172,11 @@ jobs: - name: Setup gcloud uses: google-github-actions/setup-gcloud@v1 + - name: Sync build with GCS bucket + run: | + gsutil -q -m cp -r client/build/static gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH/static + gsutil -q -m rsync -cdrj html,json,txt client/build gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH + - name: Build Function working-directory: gcp/function env: From 59bdc74835e8617aeff55f79a1474af07995a6cb Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 00:11:50 +0200 Subject: [PATCH 149/343] ci(xyz-build): use live-samples subdomain --- .github/workflows/xyz-build.yml | 2 +- libs/constants/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 4560f0753584..181f94ece863 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -79,7 +79,7 @@ jobs: # (aka. local development). Usually defaults are supposed to be for # secure production but this is an exception and default # is not insecure. - BUILD_LIVE_SAMPLES_BASE_URL: https://yari-demos.developer.allizom.xyz + BUILD_LIVE_SAMPLES_BASE_URL: https://live-samples.developer.allizom.xyz # Use the stage version of interactive examples. BUILD_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net diff --git a/libs/constants/index.js b/libs/constants/index.js index 3fe90f7eb4fc..8b768b4a077a 100644 --- a/libs/constants/index.js +++ b/libs/constants/index.js @@ -111,7 +111,7 @@ export const CSP_DIRECTIVES = { "mdn.github.io", "yari-demos.prod.mdn.mozit.cloud", "yari-demos.stage.mdn.mozit.cloud", - "yari-demos.developer.allizom.xyz", + "live-samples.developer.allizom.xyz", "jsfiddle.net", "www.youtube-nocookie.com", From a08ee671f0dbbb692832e9c649711a27fd89801f Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 00:55:40 +0200 Subject: [PATCH 150/343] fix(gcp/function): add X-Forwarded headers for Telemetry --- gcp/function/src/handlers/telemetry.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/gcp/function/src/handlers/telemetry.ts b/gcp/function/src/handlers/telemetry.ts index 6a50fe69803b..555225b44b90 100644 --- a/gcp/function/src/handlers/telemetry.ts +++ b/gcp/function/src/handlers/telemetry.ts @@ -4,4 +4,5 @@ export const proxyTelemetry = createProxyMiddleware({ target: "https://incoming.telemetry.mozilla.org", changeOrigin: true, autoRewrite: true, + xfwd: true, }); From 8a1186a881dc41b62b1b18443bca936a32ac85ac Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 02:42:47 +0200 Subject: [PATCH 151/343] fix(gcp/function): do not change origin --- .github/workflows/xyz-build.yml | 107 ------------------------- gcp/function/src/handlers/telemetry.ts | 1 - 2 files changed, 108 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 181f94ece863..7f089c996a90 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -37,18 +37,11 @@ jobs: with: repository: mdn/content path: mdn/content - # Yes, this means fetch EVERY COMMIT EVER. - # It's probably not sustainable in the far future (e.g. past 2021) - # but for now it's good enough. We'll need all the history - # so we can figure out each document's last-modified date. - fetch-depth: 0 - uses: actions/checkout@v3 with: repository: mdn/translated-content path: mdn/translated-content - # See matching warning for mdn/content checkout step - fetch-depth: 0 - uses: actions/checkout@v3 with: @@ -67,101 +60,6 @@ jobs: - name: Print information about CPU run: cat /proc/cpuinfo - - name: Build everything - env: - # Remember, the mdn/content repo got cloned into `pwd` into a - # sub-folder called "mdn/content" - CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files - CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files - CONTRIBUTOR_SPOTLIGHT_ROOT: ${{ github.workspace }}/mdn/mdn-contributor-spotlight/contributors - - # The default for this environment variable is geared for writers - # (aka. local development). Usually defaults are supposed to be for - # secure production but this is an exception and default - # is not insecure. - BUILD_LIVE_SAMPLES_BASE_URL: https://live-samples.developer.allizom.xyz - - # Use the stage version of interactive examples. - BUILD_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net - - # Now is not the time to worry about flaws. - BUILD_FLAW_LEVELS: "*:ignore" - - # This is the Google Analytics account ID for developer.mozilla.org - # If it's used on other domains (e.g. stage or dev builds), it's OK - # because ultimately Google Analytics will filter it out since the - # origin domain isn't what that account expects. - #BUILD_GOOGLE_ANALYTICS_ACCOUNT: UA-36116321-5 - - # This enables the Plus call-to-action banner and the Plus landing page - REACT_APP_ENABLE_PLUS: true - - # This adds the ability to sign in (stage only for now) - REACT_APP_DISABLE_AUTH: false - - # Use the stage version of interactive examples in react app - REACT_APP_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net - - # Firefox Accounts and SubPlat settings - REACT_APP_FXA_SIGNIN_URL: /users/fxa/login/authenticate/ - REACT_APP_FXA_SETTINGS_URL: https://accounts.stage.mozaws.net/settings/ - REACT_APP_MDN_PLUS_SUBSCRIBE_URL: https://accounts.stage.mozaws.net/subscriptions/products/prod_Jtbg9tyGyLRuB0 - REACT_APP_MDN_PLUS_5M_PLAN: price_1JFoTYKb9q6OnNsLalexa03p - REACT_APP_MDN_PLUS_5Y_PLAN: price_1JpIPwKb9q6OnNsLJLsIqMp7 - REACT_APP_MDN_PLUS_10M_PLAN: price_1K6X7gKb9q6OnNsLi44HdLcC - REACT_APP_MDN_PLUS_10Y_PLAN: price_1K6X8VKb9q6OnNsLFlUcEiu4 - - # Surveys. - REACT_APP_SURVEY_START_CONTENT_DISCOVERY_2023: 0 # stage - REACT_APP_SURVEY_END_CONTENT_DISCOVERY_2023: 1677672000000 # (new Date("2023-03-01 12:00:00Z")).getTime() - REACT_APP_SURVEY_RATE_FROM_CONTENT_DISCOVERY_2023: 0.0 - REACT_APP_SURVEY_RATE_TILL_CONTENT_DISCOVERY_2023: 0.05 # 5% - - # Telemetry. - REACT_APP_GLEAN_CHANNEL: xyz - REACT_APP_GLEAN_ENABLED: true - - # Newsletter - REACT_APP_NEWSLETTER_ENABLED: false - - # Placement - REACT_APP_PLACEMENT_ENABLED: true - - run: | - - # Info about which CONTENT_* environment variables were set and to what. - echo "CONTENT_ROOT=$CONTENT_ROOT" - echo "CONTENT_TRANSLATED_ROOT=$CONTENT_TRANSLATED_ROOT" - # Build the ServiceWorker first - yarn build:sw - yarn build:prepare - - # (July 15, 2021) This is a temporary solution. This should become an - # integrated part of 'build:prepare'. - # See https://github.com/mdn/yari/issues/4217 - yarn tool popularities - - yarn tool sync-translated-content - - for locale in en-us es fr ja ko pt-br ru zh-cn zh-tw; do - yarn build --locale $locale & - pids+=($!) - done - - tail -n 0 -qF *.log & - tail=$! - - for pid in "${pids[@]}"; do - wait $pid - done - - kill $tail - - du -sh client/build - - # Generate sitemap index file - yarn build --sitemap-index - - name: Authenticate with GCP uses: google-github-actions/auth@v0 with: @@ -172,11 +70,6 @@ jobs: - name: Setup gcloud uses: google-github-actions/setup-gcloud@v1 - - name: Sync build with GCS bucket - run: | - gsutil -q -m cp -r client/build/static gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH/static - gsutil -q -m rsync -cdrj html,json,txt client/build gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH - - name: Build Function working-directory: gcp/function env: diff --git a/gcp/function/src/handlers/telemetry.ts b/gcp/function/src/handlers/telemetry.ts index 555225b44b90..dd5d8a992844 100644 --- a/gcp/function/src/handlers/telemetry.ts +++ b/gcp/function/src/handlers/telemetry.ts @@ -2,7 +2,6 @@ import { createProxyMiddleware } from "http-proxy-middleware"; export const proxyTelemetry = createProxyMiddleware({ target: "https://incoming.telemetry.mozilla.org", - changeOrigin: true, autoRewrite: true, xfwd: true, }); From e99f0dcbd2f4fb2895fb1356b5f269f05479dd19 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 02:54:59 +0200 Subject: [PATCH 152/343] fixup! fix(gcp/function): do not change origin --- gcp/function/src/handlers/telemetry.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/gcp/function/src/handlers/telemetry.ts b/gcp/function/src/handlers/telemetry.ts index dd5d8a992844..555225b44b90 100644 --- a/gcp/function/src/handlers/telemetry.ts +++ b/gcp/function/src/handlers/telemetry.ts @@ -2,6 +2,7 @@ import { createProxyMiddleware } from "http-proxy-middleware"; export const proxyTelemetry = createProxyMiddleware({ target: "https://incoming.telemetry.mozilla.org", + changeOrigin: true, autoRewrite: true, xfwd: true, }); From e727152d3d32ae8efd19dfd6c065c101c63e6b77 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 03:05:20 +0200 Subject: [PATCH 153/343] chore(gcp/function): set timeout to 20s --- gcp/function/src/handlers/rumba.ts | 1 + gcp/function/src/handlers/telemetry.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/gcp/function/src/handlers/rumba.ts b/gcp/function/src/handlers/rumba.ts index fa2a37c51496..2eb42c1fbbf7 100644 --- a/gcp/function/src/handlers/rumba.ts +++ b/gcp/function/src/handlers/rumba.ts @@ -6,5 +6,6 @@ export const proxyRumba = createProxyMiddleware({ target: sourceUri(Source.rumba), changeOrigin: true, autoRewrite: true, + proxyTimeout: 20000, xfwd: true, }); diff --git a/gcp/function/src/handlers/telemetry.ts b/gcp/function/src/handlers/telemetry.ts index 555225b44b90..e79067573e19 100644 --- a/gcp/function/src/handlers/telemetry.ts +++ b/gcp/function/src/handlers/telemetry.ts @@ -4,5 +4,6 @@ export const proxyTelemetry = createProxyMiddleware({ target: "https://incoming.telemetry.mozilla.org", changeOrigin: true, autoRewrite: true, + proxyTimeout: 20000, xfwd: true, }); From b0bb19f7f1223be8f7bf24eb7b9a871f1ff78f72 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 03:07:25 +0200 Subject: [PATCH 154/343] chore(gcp/function): remove prepandPath: true (default value) --- gcp/function/src/handlers/content.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/gcp/function/src/handlers/content.ts b/gcp/function/src/handlers/content.ts index cb36a19b1408..446bc8fe0678 100644 --- a/gcp/function/src/handlers/content.ts +++ b/gcp/function/src/handlers/content.ts @@ -12,7 +12,6 @@ const NOT_FOUND_PATH = "en-us/_spas/404.html"; export function createContentProxy(): express.Handler { const target = sourceUri(Source.content); return createProxyMiddleware({ - prependPath: true, changeOrigin: true, target, autoRewrite: true, From f5384b4a23daa1c4abba7c6fd4ae42e6ff105ed9 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 03:08:14 +0200 Subject: [PATCH 155/343] fixup! chore(gcp/function): set timeout to 20s --- gcp/function/src/handlers/content.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/gcp/function/src/handlers/content.ts b/gcp/function/src/handlers/content.ts index 446bc8fe0678..850509d59f3e 100644 --- a/gcp/function/src/handlers/content.ts +++ b/gcp/function/src/handlers/content.ts @@ -15,6 +15,7 @@ export function createContentProxy(): express.Handler { changeOrigin: true, target, autoRewrite: true, + proxyTimeout: 20000, xfwd: true, selfHandleResponse: true, onProxyRes: responseInterceptor( From 3d14d83be1a79bcc62e4c24b9d1c52dee996819f Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 03:08:29 +0200 Subject: [PATCH 156/343] fixup! chore(gcp/function): remove prepandPath: true (default value) --- gcp/function/src/handlers/content.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/function/src/handlers/content.ts b/gcp/function/src/handlers/content.ts index 850509d59f3e..2e8c3eaa1056 100644 --- a/gcp/function/src/handlers/content.ts +++ b/gcp/function/src/handlers/content.ts @@ -12,8 +12,8 @@ const NOT_FOUND_PATH = "en-us/_spas/404.html"; export function createContentProxy(): express.Handler { const target = sourceUri(Source.content); return createProxyMiddleware({ - changeOrigin: true, target, + changeOrigin: true, autoRewrite: true, proxyTimeout: 20000, xfwd: true, From 423b8c4b57b4d704146fec6ee6c15d735600d088 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 03:26:01 +0200 Subject: [PATCH 157/343] chore(gcp/function): stop redirecting leading slash --- gcp/function/src/app.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index eca70da2ba31..45a1eba1141a 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -8,7 +8,6 @@ import { plans } from "./handlers/plans.js"; import { proxyTelemetry } from "./handlers/telemetry.js"; import { pathnameLC } from "./middlewares/pathnameLC.js"; import { resolveIndexHTML } from "./middlewares/resolveIndexHTML.js"; -import { redirectLeadingSlash } from "./middlewares/redirectLeadingSlash.js"; import { redirectMovedPages } from "./middlewares/redirectMovedPages.js"; import { redirectFundamental } from "./middlewares/redirectFundamental.js"; import { redirectLocale } from "./middlewares/redirectLocale.js"; @@ -16,7 +15,6 @@ import { redirectTrailingSlash } from "./middlewares/redirectTrailingSlash.js"; const mainRouter = Router(); const proxyContent = createContentProxy(); -mainRouter.use(redirectLeadingSlash); mainRouter.all("/api/v1/stripe/plans", plans); mainRouter.all("/api/*", proxyRumba); mainRouter.all("/admin-api/*", proxyRumba); From 48a10a0509182189538fcf2885b22231421c6e4a Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 03:53:02 +0200 Subject: [PATCH 158/343] Revert "chore(gcp/function): stop redirecting leading slash" This reverts commit 423b8c4b57b4d704146fec6ee6c15d735600d088. --- gcp/function/src/app.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 45a1eba1141a..eca70da2ba31 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -8,6 +8,7 @@ import { plans } from "./handlers/plans.js"; import { proxyTelemetry } from "./handlers/telemetry.js"; import { pathnameLC } from "./middlewares/pathnameLC.js"; import { resolveIndexHTML } from "./middlewares/resolveIndexHTML.js"; +import { redirectLeadingSlash } from "./middlewares/redirectLeadingSlash.js"; import { redirectMovedPages } from "./middlewares/redirectMovedPages.js"; import { redirectFundamental } from "./middlewares/redirectFundamental.js"; import { redirectLocale } from "./middlewares/redirectLocale.js"; @@ -15,6 +16,7 @@ import { redirectTrailingSlash } from "./middlewares/redirectTrailingSlash.js"; const mainRouter = Router(); const proxyContent = createContentProxy(); +mainRouter.use(redirectLeadingSlash); mainRouter.all("/api/v1/stripe/plans", plans); mainRouter.all("/api/*", proxyRumba); mainRouter.all("/admin-api/*", proxyRumba); From 67566fd053a15e8d91819da7525dc5fb7b18dac6 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 03:54:38 +0200 Subject: [PATCH 159/343] ci(xyz-build: reduce timeout to 30s --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 7f089c996a90..4fd0c5927139 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -91,7 +91,7 @@ jobs: --allow-unauthenticated \ --entry-point=mdnHandler \ --memory=256MB \ - --timeout=60s \ + --timeout=30s \ --set-env-vars="ORIGIN_MAIN=${{ vars.ORIGIN_MAIN }}" \ --set-env-vars="ORIGIN_LIVE_SAMPLES=${{ vars.ORIGIN_LIVE_SAMPLES }}" \ --set-env-vars="SOURCE_CONTENT=https://storage.googleapis.com/${{ vars.GCP_BUCKET_NAME }}/${{ env.BUCKET_PATH }}/" \ From 955bcd8bae28dd6155a3246d21c51c2bb64036c9 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 03:55:29 +0200 Subject: [PATCH 160/343] ci(xyz-build): increase max-instances --- .github/workflows/xyz-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 4fd0c5927139..d3d6bdaac99b 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -90,6 +90,7 @@ jobs: --trigger-http \ --allow-unauthenticated \ --entry-point=mdnHandler \ + --max-instances=1000 \ --memory=256MB \ --timeout=30s \ --set-env-vars="ORIGIN_MAIN=${{ vars.ORIGIN_MAIN }}" \ From fb8b614f5b1e1251ea485e53ce6c33cb5856e836 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 04:18:39 +0200 Subject: [PATCH 161/343] chore(gcp/function): set keep-alive on Glean proxy --- gcp/function/src/handlers/telemetry.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gcp/function/src/handlers/telemetry.ts b/gcp/function/src/handlers/telemetry.ts index e79067573e19..845b65a7aeb7 100644 --- a/gcp/function/src/handlers/telemetry.ts +++ b/gcp/function/src/handlers/telemetry.ts @@ -6,4 +6,7 @@ export const proxyTelemetry = createProxyMiddleware({ autoRewrite: true, proxyTimeout: 20000, xfwd: true, + headers: { + Connection: "keep-alive", + }, }); From 7ccacfe30648115eb9c6800001a077515d19c003 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 04:29:38 +0200 Subject: [PATCH 162/343] ci(xyz-build): set min-instances = 1 --- .github/workflows/xyz-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index d3d6bdaac99b..b328d5ac4fc1 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -90,6 +90,7 @@ jobs: --trigger-http \ --allow-unauthenticated \ --entry-point=mdnHandler \ + --min-instances=1 \ --max-instances=1000 \ --memory=256MB \ --timeout=30s \ From cc5bb48a4068bdda212f3c5b1e18e460292a1683 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 04:30:03 +0200 Subject: [PATCH 163/343] fixup! Revert "ci(xyz-build): temporarily remove build/sync" --- .github/workflows/xyz-build.yml | 107 ++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index b328d5ac4fc1..0847da28b122 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -37,11 +37,18 @@ jobs: with: repository: mdn/content path: mdn/content + # Yes, this means fetch EVERY COMMIT EVER. + # It's probably not sustainable in the far future (e.g. past 2021) + # but for now it's good enough. We'll need all the history + # so we can figure out each document's last-modified date. + fetch-depth: 0 - uses: actions/checkout@v3 with: repository: mdn/translated-content path: mdn/translated-content + # See matching warning for mdn/content checkout step + fetch-depth: 0 - uses: actions/checkout@v3 with: @@ -60,6 +67,101 @@ jobs: - name: Print information about CPU run: cat /proc/cpuinfo + - name: Build everything + env: + # Remember, the mdn/content repo got cloned into `pwd` into a + # sub-folder called "mdn/content" + CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files + CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files + CONTRIBUTOR_SPOTLIGHT_ROOT: ${{ github.workspace }}/mdn/mdn-contributor-spotlight/contributors + + # The default for this environment variable is geared for writers + # (aka. local development). Usually defaults are supposed to be for + # secure production but this is an exception and default + # is not insecure. + BUILD_LIVE_SAMPLES_BASE_URL: https://live-samples.developer.allizom.xyz + + # Use the stage version of interactive examples. + BUILD_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net + + # Now is not the time to worry about flaws. + BUILD_FLAW_LEVELS: "*:ignore" + + # This is the Google Analytics account ID for developer.mozilla.org + # If it's used on other domains (e.g. stage or dev builds), it's OK + # because ultimately Google Analytics will filter it out since the + # origin domain isn't what that account expects. + #BUILD_GOOGLE_ANALYTICS_ACCOUNT: UA-36116321-5 + + # This enables the Plus call-to-action banner and the Plus landing page + REACT_APP_ENABLE_PLUS: true + + # This adds the ability to sign in (stage only for now) + REACT_APP_DISABLE_AUTH: false + + # Use the stage version of interactive examples in react app + REACT_APP_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net + + # Firefox Accounts and SubPlat settings + REACT_APP_FXA_SIGNIN_URL: /users/fxa/login/authenticate/ + REACT_APP_FXA_SETTINGS_URL: https://accounts.stage.mozaws.net/settings/ + REACT_APP_MDN_PLUS_SUBSCRIBE_URL: https://accounts.stage.mozaws.net/subscriptions/products/prod_Jtbg9tyGyLRuB0 + REACT_APP_MDN_PLUS_5M_PLAN: price_1JFoTYKb9q6OnNsLalexa03p + REACT_APP_MDN_PLUS_5Y_PLAN: price_1JpIPwKb9q6OnNsLJLsIqMp7 + REACT_APP_MDN_PLUS_10M_PLAN: price_1K6X7gKb9q6OnNsLi44HdLcC + REACT_APP_MDN_PLUS_10Y_PLAN: price_1K6X8VKb9q6OnNsLFlUcEiu4 + + # Surveys. + REACT_APP_SURVEY_START_CONTENT_DISCOVERY_2023: 0 # stage + REACT_APP_SURVEY_END_CONTENT_DISCOVERY_2023: 1677672000000 # (new Date("2023-03-01 12:00:00Z")).getTime() + REACT_APP_SURVEY_RATE_FROM_CONTENT_DISCOVERY_2023: 0.0 + REACT_APP_SURVEY_RATE_TILL_CONTENT_DISCOVERY_2023: 0.05 # 5% + + # Telemetry. + REACT_APP_GLEAN_CHANNEL: xyz + REACT_APP_GLEAN_ENABLED: true + + # Newsletter + REACT_APP_NEWSLETTER_ENABLED: false + + # Placement + REACT_APP_PLACEMENT_ENABLED: true + + run: | + + # Info about which CONTENT_* environment variables were set and to what. + echo "CONTENT_ROOT=$CONTENT_ROOT" + echo "CONTENT_TRANSLATED_ROOT=$CONTENT_TRANSLATED_ROOT" + # Build the ServiceWorker first + yarn build:sw + yarn build:prepare + + # (July 15, 2021) This is a temporary solution. This should become an + # integrated part of 'build:prepare'. + # See https://github.com/mdn/yari/issues/4217 + yarn tool popularities + + yarn tool sync-translated-content + + for locale in en-us es fr ja ko pt-br ru zh-cn zh-tw; do + yarn build --locale $locale & + pids+=($!) + done + + tail -n 0 -qF *.log & + tail=$! + + for pid in "${pids[@]}"; do + wait $pid + done + + kill $tail + + du -sh client/build + + # Generate sitemap index file + yarn build --sitemap-index + - name: Authenticate with GCP uses: google-github-actions/auth@v0 with: @@ -70,6 +172,11 @@ jobs: - name: Setup gcloud uses: google-github-actions/setup-gcloud@v1 + - name: Sync build with GCS bucket + run: | + gsutil -q -m cp -r client/build/static gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH/static + gsutil -q -m rsync -cdrj html,json,txt client/build gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH + - name: Build Function working-directory: gcp/function env: From f4ec605708f7481ce91871a4bedd5ad11dadc575 Mon Sep 17 00:00:00 2001 From: Brett Kochendorfer Date: Thu, 6 Apr 2023 08:29:05 -0500 Subject: [PATCH 164/343] ci(stage-build): deploy Cloud Function in multiple regions (#8540) * feat: Deploy cloud function in multiple regions The existing deploy-cloud-functions GHA is limited to only running CloudFunction V1. This attempts to run the deploy command directly from gcloud instead and deploys the function in multiple regions Github Issue: https://github.com/google-github-actions/deploy-cloud-functions/issues/304 * ci(stage-build): fix function deployment Fixes typos and splits the --set-env-vars/--set-secrets options. * Update .github/workflows/stage-build.yml Co-authored-by: Claas Augner <495429+caugner@users.noreply.github.com> * Update stage-build.yml * Update .github/workflows/stage-build.yml Co-authored-by: Claas Augner <495429+caugner@users.noreply.github.com> --------- Co-authored-by: Claas Augner Co-authored-by: Claas Augner <495429+caugner@users.noreply.github.com> --- .github/workflows/stage-build.yml | 45 +++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index ed9664e8fcfd..9c2c3fa9c9cd 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -298,6 +298,51 @@ jobs: run: |- gsutil -m -h "Cache-Control:public, max-age=86400" rsync -crj html,json,txt client/build gs://content-stage-mdn/main + # Deploy Function + - name: Authenticate with GCP + uses: google-github-actions/auth@v1 + with: + token_format: access_token + service_account: deploy-stage-nonprod-mdn-ingre@${{ secrets.GCP_PROJECT_NAME }}.iam.gserviceaccount.com + workload_identity_provider: projects/${{ secrets.WIP_PROJECT_ID }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions + + - name: Setup gcloud + uses: google-github-actions/setup-gcloud@v1 + + - name: Build Function + working-directory: gcp/function + env: + CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files + CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files + run: | + npm ci + npm run build + + - name: Deploy Function + run: |- + for region in europe-west1 us-west1 asia-east1; do + gcloud functions deploy mdn-nonprod-stage-$region \ + --gen2 \ + --runtime=nodejs18 \ + --region=$region \ + --source=gcp/function \ + --trigger-http \ + --allow-unauthenticated \ + --entry-point=mdnHandler \ + --memory=256MB \ + --timeout=60s \ + --set-env-vars="ORIGIN_MAIN=developer.allizom.org" \ + --set-env-vars="ORIGIN_LIVE_SAMPLES=yari-demos.developer.allizom.org" \ + --set-env-vars="SOURCE_CONTENT=https://storage.googleapis.com/content-stage-mdn/main/" \ + --set-env-vars="SOURCE_RUMBA=https://api.developer.allizom.org/" \ + --set-secrets="KEVEL_SITE_ID=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-kevel-site-id/versions/latest" \ + --set-secrets="KEVEL_NETWORK_ID=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-kevel-network-id/versions/latest" \ + --set-secrets="SIGN_SECRET=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-sign-secret/versions/latest" \ + --set-secrets="CARBON_ZONE_KEY=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-carbon-zone-key/versions/latest" \ + --set-secrets="CARBON_FALLBACK_ENABLED=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-fallback-enabled/versions/latest" + \ + done + - name: Slack Notification if: failure() uses: rtCamp/action-slack-notify@v2 From 7326f00e3db30db293ba408ab6a4c7158f19eeb1 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 19:06:28 +0200 Subject: [PATCH 165/343] ci(xyz-build): disable build --- .github/workflows/xyz-build.yml | 100 -------------------------------- 1 file changed, 100 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 0847da28b122..cf32d04a97f0 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -67,101 +67,6 @@ jobs: - name: Print information about CPU run: cat /proc/cpuinfo - - name: Build everything - env: - # Remember, the mdn/content repo got cloned into `pwd` into a - # sub-folder called "mdn/content" - CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files - CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files - CONTRIBUTOR_SPOTLIGHT_ROOT: ${{ github.workspace }}/mdn/mdn-contributor-spotlight/contributors - - # The default for this environment variable is geared for writers - # (aka. local development). Usually defaults are supposed to be for - # secure production but this is an exception and default - # is not insecure. - BUILD_LIVE_SAMPLES_BASE_URL: https://live-samples.developer.allizom.xyz - - # Use the stage version of interactive examples. - BUILD_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net - - # Now is not the time to worry about flaws. - BUILD_FLAW_LEVELS: "*:ignore" - - # This is the Google Analytics account ID for developer.mozilla.org - # If it's used on other domains (e.g. stage or dev builds), it's OK - # because ultimately Google Analytics will filter it out since the - # origin domain isn't what that account expects. - #BUILD_GOOGLE_ANALYTICS_ACCOUNT: UA-36116321-5 - - # This enables the Plus call-to-action banner and the Plus landing page - REACT_APP_ENABLE_PLUS: true - - # This adds the ability to sign in (stage only for now) - REACT_APP_DISABLE_AUTH: false - - # Use the stage version of interactive examples in react app - REACT_APP_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net - - # Firefox Accounts and SubPlat settings - REACT_APP_FXA_SIGNIN_URL: /users/fxa/login/authenticate/ - REACT_APP_FXA_SETTINGS_URL: https://accounts.stage.mozaws.net/settings/ - REACT_APP_MDN_PLUS_SUBSCRIBE_URL: https://accounts.stage.mozaws.net/subscriptions/products/prod_Jtbg9tyGyLRuB0 - REACT_APP_MDN_PLUS_5M_PLAN: price_1JFoTYKb9q6OnNsLalexa03p - REACT_APP_MDN_PLUS_5Y_PLAN: price_1JpIPwKb9q6OnNsLJLsIqMp7 - REACT_APP_MDN_PLUS_10M_PLAN: price_1K6X7gKb9q6OnNsLi44HdLcC - REACT_APP_MDN_PLUS_10Y_PLAN: price_1K6X8VKb9q6OnNsLFlUcEiu4 - - # Surveys. - REACT_APP_SURVEY_START_CONTENT_DISCOVERY_2023: 0 # stage - REACT_APP_SURVEY_END_CONTENT_DISCOVERY_2023: 1677672000000 # (new Date("2023-03-01 12:00:00Z")).getTime() - REACT_APP_SURVEY_RATE_FROM_CONTENT_DISCOVERY_2023: 0.0 - REACT_APP_SURVEY_RATE_TILL_CONTENT_DISCOVERY_2023: 0.05 # 5% - - # Telemetry. - REACT_APP_GLEAN_CHANNEL: xyz - REACT_APP_GLEAN_ENABLED: true - - # Newsletter - REACT_APP_NEWSLETTER_ENABLED: false - - # Placement - REACT_APP_PLACEMENT_ENABLED: true - - run: | - - # Info about which CONTENT_* environment variables were set and to what. - echo "CONTENT_ROOT=$CONTENT_ROOT" - echo "CONTENT_TRANSLATED_ROOT=$CONTENT_TRANSLATED_ROOT" - # Build the ServiceWorker first - yarn build:sw - yarn build:prepare - - # (July 15, 2021) This is a temporary solution. This should become an - # integrated part of 'build:prepare'. - # See https://github.com/mdn/yari/issues/4217 - yarn tool popularities - - yarn tool sync-translated-content - - for locale in en-us es fr ja ko pt-br ru zh-cn zh-tw; do - yarn build --locale $locale & - pids+=($!) - done - - tail -n 0 -qF *.log & - tail=$! - - for pid in "${pids[@]}"; do - wait $pid - done - - kill $tail - - du -sh client/build - - # Generate sitemap index file - yarn build --sitemap-index - - name: Authenticate with GCP uses: google-github-actions/auth@v0 with: @@ -172,11 +77,6 @@ jobs: - name: Setup gcloud uses: google-github-actions/setup-gcloud@v1 - - name: Sync build with GCS bucket - run: | - gsutil -q -m cp -r client/build/static gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH/static - gsutil -q -m rsync -cdrj html,json,txt client/build gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH - - name: Build Function working-directory: gcp/function env: From f489b27b43579aae832a5edca2948aac5f0d5c86 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 19:07:00 +0200 Subject: [PATCH 166/343] chore(gcp/function): forward telemetry to stage --- gcp/function/src/handlers/telemetry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/function/src/handlers/telemetry.ts b/gcp/function/src/handlers/telemetry.ts index 845b65a7aeb7..95ab0f356762 100644 --- a/gcp/function/src/handlers/telemetry.ts +++ b/gcp/function/src/handlers/telemetry.ts @@ -1,7 +1,7 @@ import { createProxyMiddleware } from "http-proxy-middleware"; export const proxyTelemetry = createProxyMiddleware({ - target: "https://incoming.telemetry.mozilla.org", + target: "https://developer.allizom.org", changeOrigin: true, autoRewrite: true, proxyTimeout: 20000, From 4b312853026fc47603ba6c44e1eaaf0a69c48d9e Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 19:22:36 +0200 Subject: [PATCH 167/343] Revert "chore(gcp/function): forward telemetry to stage" This reverts commit f489b27b43579aae832a5edca2948aac5f0d5c86. --- gcp/function/src/handlers/telemetry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/function/src/handlers/telemetry.ts b/gcp/function/src/handlers/telemetry.ts index 95ab0f356762..845b65a7aeb7 100644 --- a/gcp/function/src/handlers/telemetry.ts +++ b/gcp/function/src/handlers/telemetry.ts @@ -1,7 +1,7 @@ import { createProxyMiddleware } from "http-proxy-middleware"; export const proxyTelemetry = createProxyMiddleware({ - target: "https://developer.allizom.org", + target: "https://incoming.telemetry.mozilla.org", changeOrigin: true, autoRewrite: true, proxyTimeout: 20000, From 0ce2dd89e71f77b4e3b6f14eb47f6c98c631e8f0 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 19:23:14 +0200 Subject: [PATCH 168/343] chore(gcp/function): set logLevel = debug --- gcp/function/src/handlers/telemetry.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/gcp/function/src/handlers/telemetry.ts b/gcp/function/src/handlers/telemetry.ts index 845b65a7aeb7..9b2d404d31e6 100644 --- a/gcp/function/src/handlers/telemetry.ts +++ b/gcp/function/src/handlers/telemetry.ts @@ -2,6 +2,7 @@ import { createProxyMiddleware } from "http-proxy-middleware"; export const proxyTelemetry = createProxyMiddleware({ target: "https://incoming.telemetry.mozilla.org", + logLevel: "debug", changeOrigin: true, autoRewrite: true, proxyTimeout: 20000, From e4f1dd3a5804ba7ac09d5e0051b2312881d0fe98 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 19:30:27 +0200 Subject: [PATCH 169/343] chore(gcp/function): set secure = false --- gcp/function/src/handlers/telemetry.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/gcp/function/src/handlers/telemetry.ts b/gcp/function/src/handlers/telemetry.ts index 9b2d404d31e6..1fc1158d4eeb 100644 --- a/gcp/function/src/handlers/telemetry.ts +++ b/gcp/function/src/handlers/telemetry.ts @@ -3,6 +3,7 @@ import { createProxyMiddleware } from "http-proxy-middleware"; export const proxyTelemetry = createProxyMiddleware({ target: "https://incoming.telemetry.mozilla.org", logLevel: "debug", + secure: false, changeOrigin: true, autoRewrite: true, proxyTimeout: 20000, From e5235161f8cec047a6036a6b28ec3ee2f029fe09 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 19:34:40 +0200 Subject: [PATCH 170/343] chore(gcp/function): test with httpbin.org --- gcp/function/src/handlers/telemetry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/function/src/handlers/telemetry.ts b/gcp/function/src/handlers/telemetry.ts index 1fc1158d4eeb..cd410980c2a5 100644 --- a/gcp/function/src/handlers/telemetry.ts +++ b/gcp/function/src/handlers/telemetry.ts @@ -1,7 +1,7 @@ import { createProxyMiddleware } from "http-proxy-middleware"; export const proxyTelemetry = createProxyMiddleware({ - target: "https://incoming.telemetry.mozilla.org", + target: "https://httpbin.org/post", logLevel: "debug", secure: false, changeOrigin: true, From f1beaaaefbc18d205747de2f3123c6232596493a Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 19:54:42 +0200 Subject: [PATCH 171/343] fixup! chore(gcp/function): test with httpbin.org --- gcp/function/src/handlers/telemetry.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gcp/function/src/handlers/telemetry.ts b/gcp/function/src/handlers/telemetry.ts index cd410980c2a5..6e00cbe30458 100644 --- a/gcp/function/src/handlers/telemetry.ts +++ b/gcp/function/src/handlers/telemetry.ts @@ -1,11 +1,14 @@ import { createProxyMiddleware } from "http-proxy-middleware"; export const proxyTelemetry = createProxyMiddleware({ - target: "https://httpbin.org/post", + target: "https://httpbin.org", logLevel: "debug", secure: false, changeOrigin: true, autoRewrite: true, + pathRewrite: function (path, req) { + return `/anything`; + }, proxyTimeout: 20000, xfwd: true, headers: { From 21b45077d97e4b32d147d32bf5d16341cc71b5dd Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 19:56:37 +0200 Subject: [PATCH 172/343] chore(gcp/function): revert telemetry proxy --- gcp/function/src/handlers/telemetry.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/gcp/function/src/handlers/telemetry.ts b/gcp/function/src/handlers/telemetry.ts index 6e00cbe30458..e79067573e19 100644 --- a/gcp/function/src/handlers/telemetry.ts +++ b/gcp/function/src/handlers/telemetry.ts @@ -1,17 +1,9 @@ import { createProxyMiddleware } from "http-proxy-middleware"; export const proxyTelemetry = createProxyMiddleware({ - target: "https://httpbin.org", - logLevel: "debug", - secure: false, + target: "https://incoming.telemetry.mozilla.org", changeOrigin: true, autoRewrite: true, - pathRewrite: function (path, req) { - return `/anything`; - }, proxyTimeout: 20000, xfwd: true, - headers: { - Connection: "keep-alive", - }, }); From a274279c27d694fe3158f44de7dcaade9301d8ef Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 21:06:12 +0200 Subject: [PATCH 173/343] chore(gcp/function): use http-proxy-middleware@beta --- gcp/function/package-lock.json | 32 ++++++++++++++++++++-------- gcp/function/package.json | 2 +- gcp/function/src/handlers/content.ts | 24 +++++++++++---------- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index 2ec0baf95f14..68b15279a73f 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -19,7 +19,7 @@ "accept-language-parser": "^1.5.0", "dotenv": "^16.0.3", "express": "^4.18.2", - "http-proxy-middleware": "^2.0.6", + "http-proxy-middleware": "^3.0.0-beta.1", "sanitize-filename": "^1.6.3" }, "devDependencies": { @@ -1491,28 +1491,42 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "version": "3.0.0-beta.1", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.0-beta.1.tgz", + "integrity": "sha512-hdiTlVVoaxncf239csnEpG5ew2lRWnoNR1PMWOO6kYulSphlrfLs5JFZtFVH3R5EUWSZNMkeUqvkvfctuWaK8A==", "dependencies": { - "@types/http-proxy": "^1.17.8", + "@types/http-proxy": "^1.17.10", + "debug": "^4.3.4", "http-proxy": "^1.18.1", "is-glob": "^4.0.1", "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" + "micromatch": "^4.0.5" }, "engines": { "node": ">=12.0.0" + } + }, + "node_modules/http-proxy-middleware/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" }, - "peerDependencies": { - "@types/express": "^4.17.13" + "engines": { + "node": ">=6.0" }, "peerDependenciesMeta": { - "@types/express": { + "supports-color": { "optional": true } } }, + "node_modules/http-proxy-middleware/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", diff --git a/gcp/function/package.json b/gcp/function/package.json index 312607f3c153..37e97ca21ab3 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -26,7 +26,7 @@ "accept-language-parser": "^1.5.0", "dotenv": "^16.0.3", "express": "^4.18.2", - "http-proxy-middleware": "^2.0.6", + "http-proxy-middleware": "^3.0.0-beta.1", "sanitize-filename": "^1.6.3" }, "devDependencies": { diff --git a/gcp/function/src/handlers/content.ts b/gcp/function/src/handlers/content.ts index 2e8c3eaa1056..442dc6d2bf01 100644 --- a/gcp/function/src/handlers/content.ts +++ b/gcp/function/src/handlers/content.ts @@ -18,17 +18,19 @@ export function createContentProxy(): express.Handler { proxyTimeout: 20000, xfwd: true, selfHandleResponse: true, - onProxyRes: responseInterceptor( - async (responseBuffer, proxyRes, req, res) => { - withProxyResponseHeaders(proxyRes, req, res); - if (proxyRes.statusCode === 404) { - const response = await fetch(`${target}${NOT_FOUND_PATH}`); - res.setHeader("Content-Type", "text/html"); - return Buffer.from(await response.arrayBuffer()); - } + on: { + proxyRes: responseInterceptor( + async (responseBuffer, proxyRes, req, res) => { + withProxyResponseHeaders(proxyRes, req, res); + if (proxyRes.statusCode === 404) { + const response = await fetch(`${target}${NOT_FOUND_PATH}`); + res.setHeader("Content-Type", "text/html"); + return Buffer.from(await response.arrayBuffer()); + } - return responseBuffer; - } - ), + return responseBuffer; + } + ), + }, }); } From d195e7a113aac8838cc42b5fce911a849e93a9a9 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 21:27:08 +0200 Subject: [PATCH 174/343] chore(glean-context): set Context.testing = true if localhost --- client/src/telemetry/glean-context.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/src/telemetry/glean-context.tsx b/client/src/telemetry/glean-context.tsx index 1973e81955ea..24d9620ef90d 100644 --- a/client/src/telemetry/glean-context.tsx +++ b/client/src/telemetry/glean-context.tsx @@ -4,6 +4,7 @@ import * as navigatorMetric from "./generated/navigator"; import * as elementMetric from "./generated/element"; import * as pings from "./generated/pings"; import Glean from "@mozilla/glean/web"; +import { Context } from "@mozilla/glean/dist/core/context"; import { CRUD_MODE, GLEAN_CHANNEL, GLEAN_DEBUG, GLEAN_ENABLED } from "../env"; import { useEffect, useRef } from "react"; import { useLocation } from "react-router"; @@ -58,6 +59,10 @@ function glean(): GleanAnalytics { const uploadEnabled = !userIsOptedOut && GLEAN_ENABLED; + if (document.location.hostname === "localhost") { + Context.testing = true; + } + Glean.initialize(GLEAN_APP_ID, uploadEnabled, { maxEvents: 1, channel: GLEAN_CHANNEL, From 7fb9ebd0eb87fa63d06a99683b4b13540d6950dc Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 21:27:18 +0200 Subject: [PATCH 175/343] Revert "ci(xyz-build): disable build" This reverts commit 7326f00e3db30db293ba408ab6a4c7158f19eeb1. --- .github/workflows/xyz-build.yml | 100 ++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index cf32d04a97f0..0847da28b122 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -67,6 +67,101 @@ jobs: - name: Print information about CPU run: cat /proc/cpuinfo + - name: Build everything + env: + # Remember, the mdn/content repo got cloned into `pwd` into a + # sub-folder called "mdn/content" + CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files + CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files + CONTRIBUTOR_SPOTLIGHT_ROOT: ${{ github.workspace }}/mdn/mdn-contributor-spotlight/contributors + + # The default for this environment variable is geared for writers + # (aka. local development). Usually defaults are supposed to be for + # secure production but this is an exception and default + # is not insecure. + BUILD_LIVE_SAMPLES_BASE_URL: https://live-samples.developer.allizom.xyz + + # Use the stage version of interactive examples. + BUILD_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net + + # Now is not the time to worry about flaws. + BUILD_FLAW_LEVELS: "*:ignore" + + # This is the Google Analytics account ID for developer.mozilla.org + # If it's used on other domains (e.g. stage or dev builds), it's OK + # because ultimately Google Analytics will filter it out since the + # origin domain isn't what that account expects. + #BUILD_GOOGLE_ANALYTICS_ACCOUNT: UA-36116321-5 + + # This enables the Plus call-to-action banner and the Plus landing page + REACT_APP_ENABLE_PLUS: true + + # This adds the ability to sign in (stage only for now) + REACT_APP_DISABLE_AUTH: false + + # Use the stage version of interactive examples in react app + REACT_APP_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net + + # Firefox Accounts and SubPlat settings + REACT_APP_FXA_SIGNIN_URL: /users/fxa/login/authenticate/ + REACT_APP_FXA_SETTINGS_URL: https://accounts.stage.mozaws.net/settings/ + REACT_APP_MDN_PLUS_SUBSCRIBE_URL: https://accounts.stage.mozaws.net/subscriptions/products/prod_Jtbg9tyGyLRuB0 + REACT_APP_MDN_PLUS_5M_PLAN: price_1JFoTYKb9q6OnNsLalexa03p + REACT_APP_MDN_PLUS_5Y_PLAN: price_1JpIPwKb9q6OnNsLJLsIqMp7 + REACT_APP_MDN_PLUS_10M_PLAN: price_1K6X7gKb9q6OnNsLi44HdLcC + REACT_APP_MDN_PLUS_10Y_PLAN: price_1K6X8VKb9q6OnNsLFlUcEiu4 + + # Surveys. + REACT_APP_SURVEY_START_CONTENT_DISCOVERY_2023: 0 # stage + REACT_APP_SURVEY_END_CONTENT_DISCOVERY_2023: 1677672000000 # (new Date("2023-03-01 12:00:00Z")).getTime() + REACT_APP_SURVEY_RATE_FROM_CONTENT_DISCOVERY_2023: 0.0 + REACT_APP_SURVEY_RATE_TILL_CONTENT_DISCOVERY_2023: 0.05 # 5% + + # Telemetry. + REACT_APP_GLEAN_CHANNEL: xyz + REACT_APP_GLEAN_ENABLED: true + + # Newsletter + REACT_APP_NEWSLETTER_ENABLED: false + + # Placement + REACT_APP_PLACEMENT_ENABLED: true + + run: | + + # Info about which CONTENT_* environment variables were set and to what. + echo "CONTENT_ROOT=$CONTENT_ROOT" + echo "CONTENT_TRANSLATED_ROOT=$CONTENT_TRANSLATED_ROOT" + # Build the ServiceWorker first + yarn build:sw + yarn build:prepare + + # (July 15, 2021) This is a temporary solution. This should become an + # integrated part of 'build:prepare'. + # See https://github.com/mdn/yari/issues/4217 + yarn tool popularities + + yarn tool sync-translated-content + + for locale in en-us es fr ja ko pt-br ru zh-cn zh-tw; do + yarn build --locale $locale & + pids+=($!) + done + + tail -n 0 -qF *.log & + tail=$! + + for pid in "${pids[@]}"; do + wait $pid + done + + kill $tail + + du -sh client/build + + # Generate sitemap index file + yarn build --sitemap-index + - name: Authenticate with GCP uses: google-github-actions/auth@v0 with: @@ -77,6 +172,11 @@ jobs: - name: Setup gcloud uses: google-github-actions/setup-gcloud@v1 + - name: Sync build with GCS bucket + run: | + gsutil -q -m cp -r client/build/static gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH/static + gsutil -q -m rsync -cdrj html,json,txt client/build gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH + - name: Build Function working-directory: gcp/function env: From c107161a821d6c922ea299f46b9f534b53ed9b48 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 22:33:33 +0200 Subject: [PATCH 176/343] Revert "chore(glean-context): set Context.testing = true if localhost" This reverts commit d195e7a113aac8838cc42b5fce911a849e93a9a9. --- client/src/telemetry/glean-context.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/client/src/telemetry/glean-context.tsx b/client/src/telemetry/glean-context.tsx index 24d9620ef90d..1973e81955ea 100644 --- a/client/src/telemetry/glean-context.tsx +++ b/client/src/telemetry/glean-context.tsx @@ -4,7 +4,6 @@ import * as navigatorMetric from "./generated/navigator"; import * as elementMetric from "./generated/element"; import * as pings from "./generated/pings"; import Glean from "@mozilla/glean/web"; -import { Context } from "@mozilla/glean/dist/core/context"; import { CRUD_MODE, GLEAN_CHANNEL, GLEAN_DEBUG, GLEAN_ENABLED } from "../env"; import { useEffect, useRef } from "react"; import { useLocation } from "react-router"; @@ -59,10 +58,6 @@ function glean(): GleanAnalytics { const uploadEnabled = !userIsOptedOut && GLEAN_ENABLED; - if (document.location.hostname === "localhost") { - Context.testing = true; - } - Glean.initialize(GLEAN_APP_ID, uploadEnabled, { maxEvents: 1, channel: GLEAN_CHANNEL, From ee2102e0ff04a50dbbf2ac4eb27782a9f1fc15fd Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 22:51:24 +0200 Subject: [PATCH 177/343] Revert "Revert "ci(xyz-build): disable build"" This reverts commit 7fb9ebd0eb87fa63d06a99683b4b13540d6950dc. --- .github/workflows/xyz-build.yml | 100 -------------------------------- 1 file changed, 100 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 0847da28b122..cf32d04a97f0 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -67,101 +67,6 @@ jobs: - name: Print information about CPU run: cat /proc/cpuinfo - - name: Build everything - env: - # Remember, the mdn/content repo got cloned into `pwd` into a - # sub-folder called "mdn/content" - CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files - CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files - CONTRIBUTOR_SPOTLIGHT_ROOT: ${{ github.workspace }}/mdn/mdn-contributor-spotlight/contributors - - # The default for this environment variable is geared for writers - # (aka. local development). Usually defaults are supposed to be for - # secure production but this is an exception and default - # is not insecure. - BUILD_LIVE_SAMPLES_BASE_URL: https://live-samples.developer.allizom.xyz - - # Use the stage version of interactive examples. - BUILD_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net - - # Now is not the time to worry about flaws. - BUILD_FLAW_LEVELS: "*:ignore" - - # This is the Google Analytics account ID for developer.mozilla.org - # If it's used on other domains (e.g. stage or dev builds), it's OK - # because ultimately Google Analytics will filter it out since the - # origin domain isn't what that account expects. - #BUILD_GOOGLE_ANALYTICS_ACCOUNT: UA-36116321-5 - - # This enables the Plus call-to-action banner and the Plus landing page - REACT_APP_ENABLE_PLUS: true - - # This adds the ability to sign in (stage only for now) - REACT_APP_DISABLE_AUTH: false - - # Use the stage version of interactive examples in react app - REACT_APP_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net - - # Firefox Accounts and SubPlat settings - REACT_APP_FXA_SIGNIN_URL: /users/fxa/login/authenticate/ - REACT_APP_FXA_SETTINGS_URL: https://accounts.stage.mozaws.net/settings/ - REACT_APP_MDN_PLUS_SUBSCRIBE_URL: https://accounts.stage.mozaws.net/subscriptions/products/prod_Jtbg9tyGyLRuB0 - REACT_APP_MDN_PLUS_5M_PLAN: price_1JFoTYKb9q6OnNsLalexa03p - REACT_APP_MDN_PLUS_5Y_PLAN: price_1JpIPwKb9q6OnNsLJLsIqMp7 - REACT_APP_MDN_PLUS_10M_PLAN: price_1K6X7gKb9q6OnNsLi44HdLcC - REACT_APP_MDN_PLUS_10Y_PLAN: price_1K6X8VKb9q6OnNsLFlUcEiu4 - - # Surveys. - REACT_APP_SURVEY_START_CONTENT_DISCOVERY_2023: 0 # stage - REACT_APP_SURVEY_END_CONTENT_DISCOVERY_2023: 1677672000000 # (new Date("2023-03-01 12:00:00Z")).getTime() - REACT_APP_SURVEY_RATE_FROM_CONTENT_DISCOVERY_2023: 0.0 - REACT_APP_SURVEY_RATE_TILL_CONTENT_DISCOVERY_2023: 0.05 # 5% - - # Telemetry. - REACT_APP_GLEAN_CHANNEL: xyz - REACT_APP_GLEAN_ENABLED: true - - # Newsletter - REACT_APP_NEWSLETTER_ENABLED: false - - # Placement - REACT_APP_PLACEMENT_ENABLED: true - - run: | - - # Info about which CONTENT_* environment variables were set and to what. - echo "CONTENT_ROOT=$CONTENT_ROOT" - echo "CONTENT_TRANSLATED_ROOT=$CONTENT_TRANSLATED_ROOT" - # Build the ServiceWorker first - yarn build:sw - yarn build:prepare - - # (July 15, 2021) This is a temporary solution. This should become an - # integrated part of 'build:prepare'. - # See https://github.com/mdn/yari/issues/4217 - yarn tool popularities - - yarn tool sync-translated-content - - for locale in en-us es fr ja ko pt-br ru zh-cn zh-tw; do - yarn build --locale $locale & - pids+=($!) - done - - tail -n 0 -qF *.log & - tail=$! - - for pid in "${pids[@]}"; do - wait $pid - done - - kill $tail - - du -sh client/build - - # Generate sitemap index file - yarn build --sitemap-index - - name: Authenticate with GCP uses: google-github-actions/auth@v0 with: @@ -172,11 +77,6 @@ jobs: - name: Setup gcloud uses: google-github-actions/setup-gcloud@v1 - - name: Sync build with GCS bucket - run: | - gsutil -q -m cp -r client/build/static gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH/static - gsutil -q -m rsync -cdrj html,json,txt client/build gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH - - name: Build Function working-directory: gcp/function env: From fe81cc43221dbcfdc9b27099d57154502e0e1b46 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 22:51:46 +0200 Subject: [PATCH 178/343] chore(gcp/function): proxy telemetry with http-proxy Instead of http-proxy-middleware. --- gcp/function/package-lock.json | 1 + gcp/function/package.json | 1 + gcp/function/src/handlers/telemetry.ts | 21 +++++++++++++-------- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index 68b15279a73f..2868c9af2ea3 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -19,6 +19,7 @@ "accept-language-parser": "^1.5.0", "dotenv": "^16.0.3", "express": "^4.18.2", + "http-proxy": "^1.18.1", "http-proxy-middleware": "^3.0.0-beta.1", "sanitize-filename": "^1.6.3" }, diff --git a/gcp/function/package.json b/gcp/function/package.json index 37e97ca21ab3..59a2fcbcb2d0 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -26,6 +26,7 @@ "accept-language-parser": "^1.5.0", "dotenv": "^16.0.3", "express": "^4.18.2", + "http-proxy": "^1.18.1", "http-proxy-middleware": "^3.0.0-beta.1", "sanitize-filename": "^1.6.3" }, diff --git a/gcp/function/src/handlers/telemetry.ts b/gcp/function/src/handlers/telemetry.ts index e79067573e19..dd8992fdf72e 100644 --- a/gcp/function/src/handlers/telemetry.ts +++ b/gcp/function/src/handlers/telemetry.ts @@ -1,9 +1,14 @@ -import { createProxyMiddleware } from "http-proxy-middleware"; +import express from "express"; +import httpProxy from "http-proxy"; -export const proxyTelemetry = createProxyMiddleware({ - target: "https://incoming.telemetry.mozilla.org", - changeOrigin: true, - autoRewrite: true, - proxyTimeout: 20000, - xfwd: true, -}); +const proxy = httpProxy.createProxyServer(); + +export const proxyTelemetry = (req: express.Request, res: express.Response) => { + proxy.web(req, res, { + target: "https://incoming.telemetry.mozilla.org", + changeOrigin: true, + autoRewrite: true, + proxyTimeout: 20000, + xfwd: true, + }); +}; From a766bfecc50ce99ee1eaf5198a9446c1b65430a6 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 22:55:26 +0200 Subject: [PATCH 179/343] fixup! chore(gcp/function): proxy telemetry with http-proxy --- gcp/function/src/handlers/telemetry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/function/src/handlers/telemetry.ts b/gcp/function/src/handlers/telemetry.ts index dd8992fdf72e..4bc6f6055cf9 100644 --- a/gcp/function/src/handlers/telemetry.ts +++ b/gcp/function/src/handlers/telemetry.ts @@ -1,4 +1,4 @@ -import express from "express"; +import type express from "express"; import httpProxy from "http-proxy"; const proxy = httpProxy.createProxyServer(); From c4c620baa8198dec4b16645449b3c371507d5f56 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 23:39:17 +0200 Subject: [PATCH 180/343] chore(gcp/function): do not transform telemetry requests --- gcp/function/src/app.ts | 3 ++- gcp/function/src/middlewares/noCacheNoTransform.ts | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 gcp/function/src/middlewares/noCacheNoTransform.ts diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index eca70da2ba31..eac4595addee 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -6,6 +6,7 @@ import { proxyKevel } from "./handlers/kevel.js"; import { proxyRumba } from "./handlers/rumba.js"; import { plans } from "./handlers/plans.js"; import { proxyTelemetry } from "./handlers/telemetry.js"; +import { noCacheNoTransform } from "./middlewares/noCacheNoTransform.js"; import { pathnameLC } from "./middlewares/pathnameLC.js"; import { resolveIndexHTML } from "./middlewares/resolveIndexHTML.js"; import { redirectLeadingSlash } from "./middlewares/redirectLeadingSlash.js"; @@ -22,7 +23,7 @@ mainRouter.all("/api/*", proxyRumba); mainRouter.all("/admin-api/*", proxyRumba); mainRouter.all("/events/fxa/*", proxyRumba); mainRouter.all("/users/fxa/*", proxyRumba); -mainRouter.all("/submit/mdn-yari/*", proxyTelemetry); +mainRouter.all("/submit/mdn-yari/*", noCacheNoTransform, proxyTelemetry); mainRouter.all("/pong/*", express.json(), proxyKevel); mainRouter.all("/pimg/*", proxyKevel); mainRouter.get("/sitemaps/*", proxyContent); diff --git a/gcp/function/src/middlewares/noCacheNoTransform.ts b/gcp/function/src/middlewares/noCacheNoTransform.ts new file mode 100644 index 000000000000..98be0f4c94ad --- /dev/null +++ b/gcp/function/src/middlewares/noCacheNoTransform.ts @@ -0,0 +1,10 @@ +import type express from "express"; + +export function noCacheNoTransform( + req: express.Request, + _res: express.Response, + next: express.NextFunction +) { + req.headers["cache-control"] = "no-cache, no-transform"; + next(); +} From d1e53477651d5799da4f81c23707a7764a30fb05 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 23:47:37 +0200 Subject: [PATCH 181/343] Revert "fixup! chore(gcp/function): proxy telemetry with http-proxy" This reverts commit a766bfecc50ce99ee1eaf5198a9446c1b65430a6. --- gcp/function/src/handlers/telemetry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/function/src/handlers/telemetry.ts b/gcp/function/src/handlers/telemetry.ts index 4bc6f6055cf9..dd8992fdf72e 100644 --- a/gcp/function/src/handlers/telemetry.ts +++ b/gcp/function/src/handlers/telemetry.ts @@ -1,4 +1,4 @@ -import type express from "express"; +import express from "express"; import httpProxy from "http-proxy"; const proxy = httpProxy.createProxyServer(); From f0f6e32abba4346477a890ef5132a5e9605daf0a Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 23:47:46 +0200 Subject: [PATCH 182/343] Revert "chore(gcp/function): proxy telemetry with http-proxy" This reverts commit fe81cc43221dbcfdc9b27099d57154502e0e1b46. --- gcp/function/package-lock.json | 1 - gcp/function/package.json | 1 - gcp/function/src/handlers/telemetry.ts | 21 ++++++++------------- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index 2868c9af2ea3..68b15279a73f 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -19,7 +19,6 @@ "accept-language-parser": "^1.5.0", "dotenv": "^16.0.3", "express": "^4.18.2", - "http-proxy": "^1.18.1", "http-proxy-middleware": "^3.0.0-beta.1", "sanitize-filename": "^1.6.3" }, diff --git a/gcp/function/package.json b/gcp/function/package.json index 59a2fcbcb2d0..37e97ca21ab3 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -26,7 +26,6 @@ "accept-language-parser": "^1.5.0", "dotenv": "^16.0.3", "express": "^4.18.2", - "http-proxy": "^1.18.1", "http-proxy-middleware": "^3.0.0-beta.1", "sanitize-filename": "^1.6.3" }, diff --git a/gcp/function/src/handlers/telemetry.ts b/gcp/function/src/handlers/telemetry.ts index dd8992fdf72e..e79067573e19 100644 --- a/gcp/function/src/handlers/telemetry.ts +++ b/gcp/function/src/handlers/telemetry.ts @@ -1,14 +1,9 @@ -import express from "express"; -import httpProxy from "http-proxy"; +import { createProxyMiddleware } from "http-proxy-middleware"; -const proxy = httpProxy.createProxyServer(); - -export const proxyTelemetry = (req: express.Request, res: express.Response) => { - proxy.web(req, res, { - target: "https://incoming.telemetry.mozilla.org", - changeOrigin: true, - autoRewrite: true, - proxyTimeout: 20000, - xfwd: true, - }); -}; +export const proxyTelemetry = createProxyMiddleware({ + target: "https://incoming.telemetry.mozilla.org", + changeOrigin: true, + autoRewrite: true, + proxyTimeout: 20000, + xfwd: true, +}); From db1b9dd97d78c4056b907cd35eaeaf55b5764934 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 23:57:32 +0200 Subject: [PATCH 183/343] chore(gcp/function): downgrade to Node.js 16 --- .github/workflows/xyz-build.yml | 2 +- gcp/function/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index cf32d04a97f0..53f900e1d31e 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -91,7 +91,7 @@ jobs: for region in europe-west1 us-west1 asia-east1; do gcloud functions deploy mdn-xyz-$region \ --gen2 \ - --runtime=nodejs18 \ + --runtime=nodejs16 \ --region=$region \ --source=gcp/function \ --trigger-http \ diff --git a/gcp/function/package.json b/gcp/function/package.json index 37e97ca21ab3..9eaa088b4a44 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -38,6 +38,6 @@ "typescript": "^4.9.5" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } } From 366c2311926a5cdad0c94fcce31273456b67f7a3 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 6 Apr 2023 23:57:32 +0200 Subject: [PATCH 184/343] chore(gcp/function): downgrade to Node.js 14 --- .github/workflows/xyz-build.yml | 2 +- gcp/function/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 53f900e1d31e..a76a60122f9d 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -91,7 +91,7 @@ jobs: for region in europe-west1 us-west1 asia-east1; do gcloud functions deploy mdn-xyz-$region \ --gen2 \ - --runtime=nodejs16 \ + --runtime=nodejs14 \ --region=$region \ --source=gcp/function \ --trigger-http \ diff --git a/gcp/function/package.json b/gcp/function/package.json index 9eaa088b4a44..aed940a7379d 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -38,6 +38,6 @@ "typescript": "^4.9.5" }, "engines": { - "node": ">=16.0.0" + "node": ">=14.0.0" } } From ff0470118ad8ee8b546d6f5db2ea14967e98ef89 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 00:19:15 +0200 Subject: [PATCH 185/343] chore(gcp/function): bump back to Node.js 18 --- .github/workflows/xyz-build.yml | 2 +- gcp/function/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index a76a60122f9d..cf32d04a97f0 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -91,7 +91,7 @@ jobs: for region in europe-west1 us-west1 asia-east1; do gcloud functions deploy mdn-xyz-$region \ --gen2 \ - --runtime=nodejs14 \ + --runtime=nodejs18 \ --region=$region \ --source=gcp/function \ --trigger-http \ diff --git a/gcp/function/package.json b/gcp/function/package.json index aed940a7379d..37e97ca21ab3 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -38,6 +38,6 @@ "typescript": "^4.9.5" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } } From e59ebd57a37196ecfa40f5610c45798c81627a55 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 00:21:17 +0200 Subject: [PATCH 186/343] chore(gcp/function): replace noCacheNoTransform with raw() middleware --- gcp/function/src/app.ts | 3 +-- gcp/function/src/middlewares/noCacheNoTransform.ts | 10 ---------- 2 files changed, 1 insertion(+), 12 deletions(-) delete mode 100644 gcp/function/src/middlewares/noCacheNoTransform.ts diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index eac4595addee..9aa742d04e89 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -6,7 +6,6 @@ import { proxyKevel } from "./handlers/kevel.js"; import { proxyRumba } from "./handlers/rumba.js"; import { plans } from "./handlers/plans.js"; import { proxyTelemetry } from "./handlers/telemetry.js"; -import { noCacheNoTransform } from "./middlewares/noCacheNoTransform.js"; import { pathnameLC } from "./middlewares/pathnameLC.js"; import { resolveIndexHTML } from "./middlewares/resolveIndexHTML.js"; import { redirectLeadingSlash } from "./middlewares/redirectLeadingSlash.js"; @@ -23,7 +22,7 @@ mainRouter.all("/api/*", proxyRumba); mainRouter.all("/admin-api/*", proxyRumba); mainRouter.all("/events/fxa/*", proxyRumba); mainRouter.all("/users/fxa/*", proxyRumba); -mainRouter.all("/submit/mdn-yari/*", noCacheNoTransform, proxyTelemetry); +mainRouter.all("/submit/mdn-yari/*", express.raw(), proxyTelemetry); mainRouter.all("/pong/*", express.json(), proxyKevel); mainRouter.all("/pimg/*", proxyKevel); mainRouter.get("/sitemaps/*", proxyContent); diff --git a/gcp/function/src/middlewares/noCacheNoTransform.ts b/gcp/function/src/middlewares/noCacheNoTransform.ts deleted file mode 100644 index 98be0f4c94ad..000000000000 --- a/gcp/function/src/middlewares/noCacheNoTransform.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type express from "express"; - -export function noCacheNoTransform( - req: express.Request, - _res: express.Response, - next: express.NextFunction -) { - req.headers["cache-control"] = "no-cache, no-transform"; - next(); -} From 872d142ffe3d3ecbd9e570224518470a0ef91125 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 00:30:33 +0200 Subject: [PATCH 187/343] chore(gcp/function): pull express.raw() up --- gcp/function/src/app.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 9aa742d04e89..882993d453a6 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -16,13 +16,14 @@ import { redirectTrailingSlash } from "./middlewares/redirectTrailingSlash.js"; const mainRouter = Router(); const proxyContent = createContentProxy(); +mainRouter.use(express.raw({ type: "*/*" })); mainRouter.use(redirectLeadingSlash); mainRouter.all("/api/v1/stripe/plans", plans); mainRouter.all("/api/*", proxyRumba); mainRouter.all("/admin-api/*", proxyRumba); mainRouter.all("/events/fxa/*", proxyRumba); mainRouter.all("/users/fxa/*", proxyRumba); -mainRouter.all("/submit/mdn-yari/*", express.raw(), proxyTelemetry); +mainRouter.all("/submit/mdn-yari/*", proxyTelemetry); mainRouter.all("/pong/*", express.json(), proxyKevel); mainRouter.all("/pimg/*", proxyKevel); mainRouter.get("/sitemaps/*", proxyContent); From 283d286085c59df2094f7aa9daec57d2569cf770 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 10:00:27 +0200 Subject: [PATCH 188/343] feat(gcp/function): support local HTTPS --- gcp/function/src/cli.ts | 22 ++++++++++++++++++++-- gcp/function/src/env.ts | 4 ++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/gcp/function/src/cli.ts b/gcp/function/src/cli.ts index f0f1a7d06237..40142fea3c1c 100644 --- a/gcp/function/src/cli.ts +++ b/gcp/function/src/cli.ts @@ -1,15 +1,33 @@ +import { readFileSync } from "node:fs"; +import { createServer as createHttpsServer } from "node:https"; +import { createServer as createHttpServer } from "node:http"; + import express from "express"; import { createHandler } from "./app.js"; -import { Origin } from "./env.js"; +import { HTTPS_CERT_FILE, HTTPS_KEY_FILE, Origin } from "./env.js"; const contentApp = express(); const contentPort = 3000; contentApp.all("*", createHandler(Origin.main)); -contentApp.listen(contentPort, () => { + +createHttpServer(contentApp).listen(contentPort, () => { console.log(`Content app listening on port ${contentPort}`); }); +if (HTTPS_CERT_FILE && HTTPS_KEY_FILE) { + const PORT = 443; + createHttpsServer( + { + key: readFileSync(HTTPS_KEY_FILE), + cert: readFileSync(HTTPS_CERT_FILE), + }, + contentApp + ).listen(PORT, () => + console.log(`Content app listening on port ${PORT} [HTTPS]`) + ); +} + const liveSampleApp = express(); const liveSamplePort = 5042; diff --git a/gcp/function/src/env.ts b/gcp/function/src/env.ts index b158d16e173c..9728625d8930 100644 --- a/gcp/function/src/env.ts +++ b/gcp/function/src/env.ts @@ -63,3 +63,7 @@ export const CARBON_ZONE_KEY = process.env["CARBON_ZONE_KEY"] ?? ""; export const CARBON_FALLBACK_ENABLED = Boolean( JSON.parse(process.env["CARBON_FALLBACK_ENABLED"] || "false") ); + +// HTTP / HTTPS. +export const HTTPS_KEY_FILE = process.env["HTTPS_KEY_FILE"] ?? ""; +export const HTTPS_CERT_FILE = process.env["HTTPS_CERT_FILE"] ?? ""; From e83b258d6e517587844d82ff781e995d9f06e7e5 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 10:02:54 +0200 Subject: [PATCH 189/343] feat(gcp/function): use nodemon to auto-restart the app --- gcp/function/package-lock.json | 233 +++++++++++++++++++++++++++++++++ gcp/function/package.json | 3 +- gcp/function/src/cli.ts | 4 +- gcp/function/src/env.ts | 3 +- 4 files changed, 238 insertions(+), 5 deletions(-) diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index 68b15279a73f..39000922eaaa 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -26,6 +26,7 @@ "@swc/core": "^1.3.38", "@types/accept-language-parser": "^1.5.3", "@types/http-proxy": "^1.17.10", + "nodemon": "^2.0.22", "npm-run-all": "^4.1.5", "ts-node": "^10.9.1", "typescript": "^4.9.5" @@ -624,6 +625,12 @@ "resolved": "src/internal/slug-utils", "link": true }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, "node_modules/accept-language-parser": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/accept-language-parser/-/accept-language-parser-1.5.0.tgz", @@ -736,6 +743,19 @@ "node": ">=4" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -769,6 +789,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -846,6 +875,33 @@ "node": ">=4" } }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/cloudevents": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/cloudevents/-/cloudevents-6.0.4.tgz", @@ -1287,6 +1343,20 @@ "node": ">= 0.6" } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -1348,6 +1418,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/globalthis": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", @@ -1571,6 +1653,12 @@ "node": ">=0.10.0" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1644,6 +1732,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -2096,6 +2196,73 @@ } } }, + "node_modules/nodemon": { + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", + "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nodemon/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -2115,6 +2282,15 @@ "semver": "bin/semver" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npm-run-all": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", @@ -2349,6 +2525,12 @@ "node": ">= 0.10" } }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -2431,6 +2613,18 @@ "node": ">=8" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", @@ -2627,6 +2821,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "dependencies": { + "semver": "~7.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/spdx-correct": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", @@ -2758,6 +2973,18 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -2881,6 +3108,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, "node_modules/unfetch": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", diff --git a/gcp/function/package.json b/gcp/function/package.json index 37e97ca21ab3..a95b6e1c01f6 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -13,7 +13,7 @@ "copy-internal": "rm -rf ./src/internal && cp -R ../../libs ./src/internal", "package": "zip -r -X f.zip . -i package.json .env 'src/*.js' 'src/handlers/*.js' 'src/internal'", "prepare": "[ ! -e ../../libs ] || npm run copy-internal", - "start": "ts-node src/cli.ts" + "start": "nodemon --watch src --exec ts-node src/cli.ts" }, "dependencies": { "@adzerk/decision-sdk": "^1.0.0-beta.20", @@ -33,6 +33,7 @@ "@swc/core": "^1.3.38", "@types/accept-language-parser": "^1.5.3", "@types/http-proxy": "^1.17.10", + "nodemon": "^2.0.22", "npm-run-all": "^4.1.5", "ts-node": "^10.9.1", "typescript": "^4.9.5" diff --git a/gcp/function/src/cli.ts b/gcp/function/src/cli.ts index 40142fea3c1c..7e0bc570356d 100644 --- a/gcp/function/src/cli.ts +++ b/gcp/function/src/cli.ts @@ -1,6 +1,5 @@ import { readFileSync } from "node:fs"; import { createServer as createHttpsServer } from "node:https"; -import { createServer as createHttpServer } from "node:http"; import express from "express"; import { createHandler } from "./app.js"; @@ -10,8 +9,7 @@ const contentApp = express(); const contentPort = 3000; contentApp.all("*", createHandler(Origin.main)); - -createHttpServer(contentApp).listen(contentPort, () => { +contentApp.listen(contentPort, () => { console.log(`Content app listening on port ${contentPort}`); }); diff --git a/gcp/function/src/env.ts b/gcp/function/src/env.ts index 9728625d8930..84101d0e5e23 100644 --- a/gcp/function/src/env.ts +++ b/gcp/function/src/env.ts @@ -64,6 +64,7 @@ export const CARBON_FALLBACK_ENABLED = Boolean( JSON.parse(process.env["CARBON_FALLBACK_ENABLED"] || "false") ); -// HTTP / HTTPS. +// HTTPS. +// (Use https://github.com/FiloSottile/mkcert to generate a locally-trusted certificate.) export const HTTPS_KEY_FILE = process.env["HTTPS_KEY_FILE"] ?? ""; export const HTTPS_CERT_FILE = process.env["HTTPS_CERT_FILE"] ?? ""; From 2dd34592468583744872a99b1678811e3ee0d749 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 10:08:24 +0200 Subject: [PATCH 190/343] Revert "chore(gcp/function): use http-proxy-middleware@beta" This reverts commit a274279c27d694fe3158f44de7dcaade9301d8ef. --- gcp/function/package-lock.json | 32 ++++++++-------------------- gcp/function/package.json | 2 +- gcp/function/src/handlers/content.ts | 24 ++++++++++----------- 3 files changed, 21 insertions(+), 37 deletions(-) diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index 39000922eaaa..d0b48265ecff 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -19,7 +19,7 @@ "accept-language-parser": "^1.5.0", "dotenv": "^16.0.3", "express": "^4.18.2", - "http-proxy-middleware": "^3.0.0-beta.1", + "http-proxy-middleware": "^2.0.6", "sanitize-filename": "^1.6.3" }, "devDependencies": { @@ -1573,42 +1573,28 @@ } }, "node_modules/http-proxy-middleware": { - "version": "3.0.0-beta.1", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.0-beta.1.tgz", - "integrity": "sha512-hdiTlVVoaxncf239csnEpG5ew2lRWnoNR1PMWOO6kYulSphlrfLs5JFZtFVH3R5EUWSZNMkeUqvkvfctuWaK8A==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", "dependencies": { - "@types/http-proxy": "^1.17.10", - "debug": "^4.3.4", + "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", "is-glob": "^4.0.1", "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.5" + "micromatch": "^4.0.2" }, "engines": { "node": ">=12.0.0" - } - }, - "node_modules/http-proxy-middleware/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" }, - "engines": { - "node": ">=6.0" + "peerDependencies": { + "@types/express": "^4.17.13" }, "peerDependenciesMeta": { - "supports-color": { + "@types/express": { "optional": true } } }, - "node_modules/http-proxy-middleware/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", diff --git a/gcp/function/package.json b/gcp/function/package.json index a95b6e1c01f6..0986eec50d95 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -26,7 +26,7 @@ "accept-language-parser": "^1.5.0", "dotenv": "^16.0.3", "express": "^4.18.2", - "http-proxy-middleware": "^3.0.0-beta.1", + "http-proxy-middleware": "^2.0.6", "sanitize-filename": "^1.6.3" }, "devDependencies": { diff --git a/gcp/function/src/handlers/content.ts b/gcp/function/src/handlers/content.ts index 442dc6d2bf01..2e8c3eaa1056 100644 --- a/gcp/function/src/handlers/content.ts +++ b/gcp/function/src/handlers/content.ts @@ -18,19 +18,17 @@ export function createContentProxy(): express.Handler { proxyTimeout: 20000, xfwd: true, selfHandleResponse: true, - on: { - proxyRes: responseInterceptor( - async (responseBuffer, proxyRes, req, res) => { - withProxyResponseHeaders(proxyRes, req, res); - if (proxyRes.statusCode === 404) { - const response = await fetch(`${target}${NOT_FOUND_PATH}`); - res.setHeader("Content-Type", "text/html"); - return Buffer.from(await response.arrayBuffer()); - } - - return responseBuffer; + onProxyRes: responseInterceptor( + async (responseBuffer, proxyRes, req, res) => { + withProxyResponseHeaders(proxyRes, req, res); + if (proxyRes.statusCode === 404) { + const response = await fetch(`${target}${NOT_FOUND_PATH}`); + res.setHeader("Content-Type", "text/html"); + return Buffer.from(await response.arrayBuffer()); } - ), - }, + + return responseBuffer; + } + ), }); } From dda902f8167c8d7025277d445e95f2986f10e5cd Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 10:46:44 +0200 Subject: [PATCH 191/343] feat(gcp/function): add DEBUG_TELEMETRY option --- gcp/function/src/cli.ts | 30 +++++++++++++++++++++++++- gcp/function/src/env.ts | 4 ++++ gcp/function/src/handlers/telemetry.ts | 5 ++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/gcp/function/src/cli.ts b/gcp/function/src/cli.ts index 7e0bc570356d..aacce7c0b4f7 100644 --- a/gcp/function/src/cli.ts +++ b/gcp/function/src/cli.ts @@ -2,8 +2,14 @@ import { readFileSync } from "node:fs"; import { createServer as createHttpsServer } from "node:https"; import express from "express"; + import { createHandler } from "./app.js"; -import { HTTPS_CERT_FILE, HTTPS_KEY_FILE, Origin } from "./env.js"; +import { + DEBUG_TELEMETRY, + HTTPS_CERT_FILE, + HTTPS_KEY_FILE, + Origin, +} from "./env.js"; const contentApp = express(); const contentPort = 3000; @@ -33,3 +39,25 @@ liveSampleApp.all("*", createHandler(Origin.liveSamples)); liveSampleApp.listen(liveSamplePort, () => { console.log(`Sample app listening on port ${liveSamplePort}`); }); + +if (DEBUG_TELEMETRY) { + const debugApp = express(); + debugApp.all( + "*", + express.json(), + async (req: express.Request, res: express.Response) => { + const { method, url, query, headers, body } = req; + const payload = { + method, + url, + headers, + query, + body, + }; + res.setHeader("Content-Type", "application/json"), + res.write(JSON.stringify(payload)); + res.end(); + } + ); + debugApp.listen(8888); +} diff --git a/gcp/function/src/env.ts b/gcp/function/src/env.ts index 84101d0e5e23..55a1b1395022 100644 --- a/gcp/function/src/env.ts +++ b/gcp/function/src/env.ts @@ -68,3 +68,7 @@ export const CARBON_FALLBACK_ENABLED = Boolean( // (Use https://github.com/FiloSottile/mkcert to generate a locally-trusted certificate.) export const HTTPS_KEY_FILE = process.env["HTTPS_KEY_FILE"] ?? ""; export const HTTPS_CERT_FILE = process.env["HTTPS_CERT_FILE"] ?? ""; + +export const DEBUG_TELEMETRY = Boolean( + JSON.parse(process.env["DEBUG_TELEMETRY"] || "false") +); diff --git a/gcp/function/src/handlers/telemetry.ts b/gcp/function/src/handlers/telemetry.ts index e79067573e19..6ed562d9604b 100644 --- a/gcp/function/src/handlers/telemetry.ts +++ b/gcp/function/src/handlers/telemetry.ts @@ -1,7 +1,10 @@ import { createProxyMiddleware } from "http-proxy-middleware"; +import { DEBUG_TELEMETRY } from "../env.js"; export const proxyTelemetry = createProxyMiddleware({ - target: "https://incoming.telemetry.mozilla.org", + target: DEBUG_TELEMETRY + ? "http://localhost:8888/" + : "https://incoming.telemetry.mozilla.org", changeOrigin: true, autoRewrite: true, proxyTimeout: 20000, From 27245a93993caff194b9dbb79d1b503728dfcc4a Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 11:08:47 +0200 Subject: [PATCH 192/343] chore(gcp/function): remove express.raw() --- gcp/function/src/app.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 882993d453a6..eca70da2ba31 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -16,7 +16,6 @@ import { redirectTrailingSlash } from "./middlewares/redirectTrailingSlash.js"; const mainRouter = Router(); const proxyContent = createContentProxy(); -mainRouter.use(express.raw({ type: "*/*" })); mainRouter.use(redirectLeadingSlash); mainRouter.all("/api/v1/stripe/plans", plans); mainRouter.all("/api/*", proxyRumba); From 667706d2cc15d805152602e052e5e695080011f9 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 11:13:25 +0200 Subject: [PATCH 193/343] fixup! feat(gcp/function): add DEBUG_TELEMETRY option --- gcp/function/src/cli.ts | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/gcp/function/src/cli.ts b/gcp/function/src/cli.ts index aacce7c0b4f7..5def0baf58ae 100644 --- a/gcp/function/src/cli.ts +++ b/gcp/function/src/cli.ts @@ -42,22 +42,17 @@ liveSampleApp.listen(liveSamplePort, () => { if (DEBUG_TELEMETRY) { const debugApp = express(); - debugApp.all( - "*", - express.json(), - async (req: express.Request, res: express.Response) => { - const { method, url, query, headers, body } = req; - const payload = { - method, - url, - headers, - query, - body, - }; - res.setHeader("Content-Type", "application/json"), - res.write(JSON.stringify(payload)); - res.end(); - } - ); + debugApp.all("*", async (req: express.Request, res: express.Response) => { + const { method, url, query, headers } = req; + const payload = { + method, + url, + headers, + query, + }; + res.setHeader("Content-Type", "application/json"), + res.write(JSON.stringify(payload)); + res.end(); + }); debugApp.listen(8888); } From 7340364abc7522f1ea7d593597055db0b11d43fa Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 11:17:01 +0200 Subject: [PATCH 194/343] fixup! feat(gcp/function): use nodemon to auto-restart the app --- gcp/function/package.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/gcp/function/package.json b/gcp/function/package.json index 0986eec50d95..f5770beea1b9 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -13,7 +13,7 @@ "copy-internal": "rm -rf ./src/internal && cp -R ../../libs ./src/internal", "package": "zip -r -X f.zip . -i package.json .env 'src/*.js' 'src/handlers/*.js' 'src/internal'", "prepare": "[ ! -e ../../libs ] || npm run copy-internal", - "start": "nodemon --watch src --exec ts-node src/cli.ts" + "start": "nodemon --exec ts-node src/cli.ts" }, "dependencies": { "@adzerk/decision-sdk": "^1.0.0-beta.20", @@ -40,5 +40,11 @@ }, "engines": { "node": ">=18.0.0" + }, + "nodemonConfig": { + "watch": [ + ".env", + "src" + ] } } From 6a49c4601ad6e80bc76541cdeb2e7dfcff0670ca Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 12:17:36 +0200 Subject: [PATCH 195/343] chore(gcp/function): use compression() for telemetry --- gcp/function/package-lock.json | 60 ++++++++++++++++++++++++++++++++++ gcp/function/package.json | 2 ++ gcp/function/src/app.ts | 4 ++- 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index d0b48265ecff..4db52b39b485 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -17,6 +17,7 @@ "@yari-internal/locale-utils": "file:src/internal/locale-utils", "@yari-internal/slug-utils": "file:src/internal/slug-utils", "accept-language-parser": "^1.5.0", + "compression": "^1.7.4", "dotenv": "^16.0.3", "express": "^4.18.2", "http-proxy-middleware": "^2.0.6", @@ -25,6 +26,7 @@ "devDependencies": { "@swc/core": "^1.3.38", "@types/accept-language-parser": "^1.5.3", + "@types/compression": "^1.7.2", "@types/http-proxy": "^1.17.10", "nodemon": "^2.0.22", "npm-run-all": "^4.1.5", @@ -538,6 +540,15 @@ "@types/node": "*" } }, + "node_modules/@types/compression": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.2.tgz", + "integrity": "sha512-lwEL4M/uAGWngWFLSG87ZDr2kLrbuR8p7X+QZB1OQlT+qkHsCPDVFnHPyXf4Vyl4yDDorNY+mAhosxkCvppatg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -941,6 +952,47 @@ "node": ">= 0.8" } }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2362,6 +2414,14 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", diff --git a/gcp/function/package.json b/gcp/function/package.json index f5770beea1b9..8e616fc6719c 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -24,6 +24,7 @@ "@yari-internal/locale-utils": "file:src/internal/locale-utils", "@yari-internal/slug-utils": "file:src/internal/slug-utils", "accept-language-parser": "^1.5.0", + "compression": "^1.7.4", "dotenv": "^16.0.3", "express": "^4.18.2", "http-proxy-middleware": "^2.0.6", @@ -32,6 +33,7 @@ "devDependencies": { "@swc/core": "^1.3.38", "@types/accept-language-parser": "^1.5.3", + "@types/compression": "^1.7.2", "@types/http-proxy": "^1.17.10", "nodemon": "^2.0.22", "npm-run-all": "^4.1.5", diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index eca70da2ba31..974caed55ed5 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -1,5 +1,7 @@ import express from "express"; import { Router } from "express"; +import compression from "compression"; + import { Origin, origin } from "./env.js"; import { createContentProxy } from "./handlers/content.js"; import { proxyKevel } from "./handlers/kevel.js"; @@ -22,7 +24,7 @@ mainRouter.all("/api/*", proxyRumba); mainRouter.all("/admin-api/*", proxyRumba); mainRouter.all("/events/fxa/*", proxyRumba); mainRouter.all("/users/fxa/*", proxyRumba); -mainRouter.all("/submit/mdn-yari/*", proxyTelemetry); +mainRouter.all("/submit/mdn-yari/*", compression(), proxyTelemetry); mainRouter.all("/pong/*", express.json(), proxyKevel); mainRouter.all("/pimg/*", proxyKevel); mainRouter.get("/sitemaps/*", proxyContent); From 1cd5729d596acd15c576a0e1c807c79b9baef047 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 13:07:34 +0200 Subject: [PATCH 196/343] chore(gcp/function): use fixRequestBody --- gcp/function/src/handlers/telemetry.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gcp/function/src/handlers/telemetry.ts b/gcp/function/src/handlers/telemetry.ts index 6ed562d9604b..916464d6eaaa 100644 --- a/gcp/function/src/handlers/telemetry.ts +++ b/gcp/function/src/handlers/telemetry.ts @@ -1,4 +1,4 @@ -import { createProxyMiddleware } from "http-proxy-middleware"; +import { createProxyMiddleware, fixRequestBody } from "http-proxy-middleware"; import { DEBUG_TELEMETRY } from "../env.js"; export const proxyTelemetry = createProxyMiddleware({ @@ -9,4 +9,5 @@ export const proxyTelemetry = createProxyMiddleware({ autoRewrite: true, proxyTimeout: 20000, xfwd: true, + onProxyReq: fixRequestBody, }); From de56d8ec16647349b6805541c858d7c7daf97517 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 13:09:48 +0200 Subject: [PATCH 197/343] Revert "Revert "Revert "ci(xyz-build): disable build""" This reverts commit ee2102e0ff04a50dbbf2ac4eb27782a9f1fc15fd. --- .github/workflows/xyz-build.yml | 100 ++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index cf32d04a97f0..0847da28b122 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -67,6 +67,101 @@ jobs: - name: Print information about CPU run: cat /proc/cpuinfo + - name: Build everything + env: + # Remember, the mdn/content repo got cloned into `pwd` into a + # sub-folder called "mdn/content" + CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files + CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files + CONTRIBUTOR_SPOTLIGHT_ROOT: ${{ github.workspace }}/mdn/mdn-contributor-spotlight/contributors + + # The default for this environment variable is geared for writers + # (aka. local development). Usually defaults are supposed to be for + # secure production but this is an exception and default + # is not insecure. + BUILD_LIVE_SAMPLES_BASE_URL: https://live-samples.developer.allizom.xyz + + # Use the stage version of interactive examples. + BUILD_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net + + # Now is not the time to worry about flaws. + BUILD_FLAW_LEVELS: "*:ignore" + + # This is the Google Analytics account ID for developer.mozilla.org + # If it's used on other domains (e.g. stage or dev builds), it's OK + # because ultimately Google Analytics will filter it out since the + # origin domain isn't what that account expects. + #BUILD_GOOGLE_ANALYTICS_ACCOUNT: UA-36116321-5 + + # This enables the Plus call-to-action banner and the Plus landing page + REACT_APP_ENABLE_PLUS: true + + # This adds the ability to sign in (stage only for now) + REACT_APP_DISABLE_AUTH: false + + # Use the stage version of interactive examples in react app + REACT_APP_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net + + # Firefox Accounts and SubPlat settings + REACT_APP_FXA_SIGNIN_URL: /users/fxa/login/authenticate/ + REACT_APP_FXA_SETTINGS_URL: https://accounts.stage.mozaws.net/settings/ + REACT_APP_MDN_PLUS_SUBSCRIBE_URL: https://accounts.stage.mozaws.net/subscriptions/products/prod_Jtbg9tyGyLRuB0 + REACT_APP_MDN_PLUS_5M_PLAN: price_1JFoTYKb9q6OnNsLalexa03p + REACT_APP_MDN_PLUS_5Y_PLAN: price_1JpIPwKb9q6OnNsLJLsIqMp7 + REACT_APP_MDN_PLUS_10M_PLAN: price_1K6X7gKb9q6OnNsLi44HdLcC + REACT_APP_MDN_PLUS_10Y_PLAN: price_1K6X8VKb9q6OnNsLFlUcEiu4 + + # Surveys. + REACT_APP_SURVEY_START_CONTENT_DISCOVERY_2023: 0 # stage + REACT_APP_SURVEY_END_CONTENT_DISCOVERY_2023: 1677672000000 # (new Date("2023-03-01 12:00:00Z")).getTime() + REACT_APP_SURVEY_RATE_FROM_CONTENT_DISCOVERY_2023: 0.0 + REACT_APP_SURVEY_RATE_TILL_CONTENT_DISCOVERY_2023: 0.05 # 5% + + # Telemetry. + REACT_APP_GLEAN_CHANNEL: xyz + REACT_APP_GLEAN_ENABLED: true + + # Newsletter + REACT_APP_NEWSLETTER_ENABLED: false + + # Placement + REACT_APP_PLACEMENT_ENABLED: true + + run: | + + # Info about which CONTENT_* environment variables were set and to what. + echo "CONTENT_ROOT=$CONTENT_ROOT" + echo "CONTENT_TRANSLATED_ROOT=$CONTENT_TRANSLATED_ROOT" + # Build the ServiceWorker first + yarn build:sw + yarn build:prepare + + # (July 15, 2021) This is a temporary solution. This should become an + # integrated part of 'build:prepare'. + # See https://github.com/mdn/yari/issues/4217 + yarn tool popularities + + yarn tool sync-translated-content + + for locale in en-us es fr ja ko pt-br ru zh-cn zh-tw; do + yarn build --locale $locale & + pids+=($!) + done + + tail -n 0 -qF *.log & + tail=$! + + for pid in "${pids[@]}"; do + wait $pid + done + + kill $tail + + du -sh client/build + + # Generate sitemap index file + yarn build --sitemap-index + - name: Authenticate with GCP uses: google-github-actions/auth@v0 with: @@ -77,6 +172,11 @@ jobs: - name: Setup gcloud uses: google-github-actions/setup-gcloud@v1 + - name: Sync build with GCS bucket + run: | + gsutil -q -m cp -r client/build/static gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH/static + gsutil -q -m rsync -cdrj html,json,txt client/build gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH + - name: Build Function working-directory: gcp/function env: From 363c0726bd6ede25724545d9f30862d2ff31ccc2 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 13:12:32 +0200 Subject: [PATCH 198/343] ci(xyz-build): skip steps via env vars --- .github/workflows/xyz-build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 0847da28b122..ebac40e16a8d 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -68,6 +68,7 @@ jobs: run: cat /proc/cpuinfo - name: Build everything + if: ! ${{ vars.SKIP_BUILD }} env: # Remember, the mdn/content repo got cloned into `pwd` into a # sub-folder called "mdn/content" @@ -173,11 +174,13 @@ jobs: uses: google-github-actions/setup-gcloud@v1 - name: Sync build with GCS bucket + if: ! ${{ vars.SKIP_BUILD }} run: | gsutil -q -m cp -r client/build/static gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH/static gsutil -q -m rsync -cdrj html,json,txt client/build gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH - name: Build Function + if: ! ${{ vars.SKIP_FUNCTION }} working-directory: gcp/function env: CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files @@ -187,6 +190,7 @@ jobs: npm run build - name: Deploy Function + if: ! ${{ vars.SKIP_FUNCTION }} run: |- for region in europe-west1 us-west1 asia-east1; do gcloud functions deploy mdn-xyz-$region \ @@ -218,5 +222,6 @@ jobs: done - name: Invalidate CDN + if: ! ${{ vars.SKIP_INVALIDATE }} run: |- gcloud compute url-maps invalidate-cdn-cache ${{ vars.GCP_LOAD_BALANCER_NAME }} --path "/*" From a6f1348edef10278993382e58b65c518dee203e5 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 13:14:14 +0200 Subject: [PATCH 199/343] fixup! ci(xyz-build): skip steps via env vars --- .github/workflows/xyz-build.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index ebac40e16a8d..7291315320ba 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -68,7 +68,7 @@ jobs: run: cat /proc/cpuinfo - name: Build everything - if: ! ${{ vars.SKIP_BUILD }} + if: ${{ ! vars.SKIP_BUILD }} env: # Remember, the mdn/content repo got cloned into `pwd` into a # sub-folder called "mdn/content" @@ -174,13 +174,13 @@ jobs: uses: google-github-actions/setup-gcloud@v1 - name: Sync build with GCS bucket - if: ! ${{ vars.SKIP_BUILD }} + if: ${{ ! vars.SKIP_BUILD }} run: | gsutil -q -m cp -r client/build/static gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH/static gsutil -q -m rsync -cdrj html,json,txt client/build gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH - name: Build Function - if: ! ${{ vars.SKIP_FUNCTION }} + if: ${{ ! vars.SKIP_FUNCTION }} working-directory: gcp/function env: CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files @@ -190,7 +190,7 @@ jobs: npm run build - name: Deploy Function - if: ! ${{ vars.SKIP_FUNCTION }} + if: ${{ ! vars.SKIP_FUNCTION }} run: |- for region in europe-west1 us-west1 asia-east1; do gcloud functions deploy mdn-xyz-$region \ @@ -222,6 +222,6 @@ jobs: done - name: Invalidate CDN - if: ! ${{ vars.SKIP_INVALIDATE }} + if: ${{ ! vars.SKIP_INVALIDATE }} run: |- gcloud compute url-maps invalidate-cdn-cache ${{ vars.GCP_LOAD_BALANCER_NAME }} --path "/*" From cb880223a19d8c763688413fe90a6e7c6e8815cb Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 15:45:24 +0200 Subject: [PATCH 200/343] feat(gcp/function): use @google-cloud/functions-framework - Builds `redirects.json` on `npm run install`. - Follows: https://github.com/GoogleCloudPlatform/functions-framework-nodejs/blob/5f1f7a95edc69a34bc41452751e22121ab8b4a00/docs/typescript.md --- gcp/function/.gcloudignore | 6 ++---- gcp/function/.gitignore | 2 ++ gcp/function/package.json | 12 +++++++----- gcp/function/redirects.json | 1 - gcp/function/src/index.ts | 4 ++-- 5 files changed, 13 insertions(+), 12 deletions(-) delete mode 100644 gcp/function/redirects.json diff --git a/gcp/function/.gcloudignore b/gcp/function/.gcloudignore index e419315ad70a..aafa4b335a32 100644 --- a/gcp/function/.gcloudignore +++ b/gcp/function/.gcloudignore @@ -14,7 +14,5 @@ .git .gitignore -node_modules -src/**/*.ts -!#include:.gitignore - +#!include:.gitignore +!redirects.json diff --git a/gcp/function/.gitignore b/gcp/function/.gitignore index b3397798ee69..a8c548947e16 100644 --- a/gcp/function/.gitignore +++ b/gcp/function/.gitignore @@ -1,3 +1,5 @@ +node_modules .env* +redirects.json src/**/*.js src/internal diff --git a/gcp/function/package.json b/gcp/function/package.json index 8e616fc6719c..fa6d2907fa83 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -8,12 +8,14 @@ "type": "module", "main": "src/index.js", "scripts": { - "build": "tsc -b && ts-node src/build.ts", - "clean": "tsc -b --clean && git checkout -- redirects.json", + "build": "tsc -b", "copy-internal": "rm -rf ./src/internal && cp -R ../../libs ./src/internal", - "package": "zip -r -X f.zip . -i package.json .env 'src/*.js' 'src/handlers/*.js' 'src/internal'", - "prepare": "[ ! -e ../../libs ] || npm run copy-internal", - "start": "nodemon --exec ts-node src/cli.ts" + "gcp-build": "npm run build", + "prepare": "([ ! -e ../../libs ] || npm run copy-internal) && ([ -e redirects.json ] || npm run redirects)", + "prestart": "npm run build", + "redirects": "ts-node src/build.ts", + "start": "functions-framework --target=mdnHandler", + "watch": "nodemon --exec npm run start" }, "dependencies": { "@adzerk/decision-sdk": "^1.0.0-beta.20", diff --git a/gcp/function/redirects.json b/gcp/function/redirects.json deleted file mode 100644 index 0967ef424bce..000000000000 --- a/gcp/function/redirects.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/gcp/function/src/index.ts b/gcp/function/src/index.ts index 47d87546555f..789e9293275e 100644 --- a/gcp/function/src/index.ts +++ b/gcp/function/src/index.ts @@ -1,5 +1,5 @@ import { createHandler } from "./app.js"; -import functions from "@google-cloud/functions-framework"; +import { http } from "@google-cloud/functions-framework"; import { GCPFunction } from "@sentry/serverless"; let handler = createHandler(); @@ -9,4 +9,4 @@ if (process.env["SENTRY_DSN"]) { handler = GCPFunction.wrapHttpFunction(handler); } -functions.http("mdnHandler", handler); +http("mdnHandler", handler); From c38f4d8e2e13d8a46bd7387adf75438642339112 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 16:54:54 +0200 Subject: [PATCH 201/343] refactor(gcp/function): merge routers + add HTTPS proxy --- gcp/function/Procfile | 2 + gcp/function/package-lock.json | 1 + gcp/function/package.json | 9 +- gcp/function/src/app.ts | 91 ++++++++++--------- gcp/function/src/cli.ts | 58 ------------ gcp/function/src/env.ts | 10 ++ gcp/function/src/middlewares/requireOrigin.ts | 14 +++ gcp/function/src/proxy.ts | 45 +++++++++ gcp/function/tsconfig.json | 1 - 9 files changed, 124 insertions(+), 107 deletions(-) create mode 100644 gcp/function/Procfile delete mode 100644 gcp/function/src/cli.ts create mode 100644 gcp/function/src/middlewares/requireOrigin.ts create mode 100644 gcp/function/src/proxy.ts diff --git a/gcp/function/Procfile b/gcp/function/Procfile new file mode 100644 index 000000000000..f8544f00c5ca --- /dev/null +++ b/gcp/function/Procfile @@ -0,0 +1,2 @@ +proxy: npm run proxy +server: npm run server:watch \ No newline at end of file diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index 4db52b39b485..37480cfa9726 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -28,6 +28,7 @@ "@types/accept-language-parser": "^1.5.3", "@types/compression": "^1.7.2", "@types/http-proxy": "^1.17.10", + "http-proxy": "^1.18.1", "nodemon": "^2.0.22", "npm-run-all": "^4.1.5", "ts-node": "^10.9.1", diff --git a/gcp/function/package.json b/gcp/function/package.json index fa6d2907fa83..1ea2f1a372b3 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -12,10 +12,11 @@ "copy-internal": "rm -rf ./src/internal && cp -R ../../libs ./src/internal", "gcp-build": "npm run build", "prepare": "([ ! -e ../../libs ] || npm run copy-internal) && ([ -e redirects.json ] || npm run redirects)", - "prestart": "npm run build", + "proxy": "ts-node src/proxy.ts", "redirects": "ts-node src/build.ts", - "start": "functions-framework --target=mdnHandler", - "watch": "nodemon --exec npm run start" + "server": "npm run build && functions-framework --target=mdnHandler", + "server:watch": "nodemon --exec npm run server", + "start": "nf start" }, "dependencies": { "@adzerk/decision-sdk": "^1.0.0-beta.20", @@ -37,6 +38,7 @@ "@types/accept-language-parser": "^1.5.3", "@types/compression": "^1.7.2", "@types/http-proxy": "^1.17.10", + "http-proxy": "^1.18.1", "nodemon": "^2.0.22", "npm-run-all": "^4.1.5", "ts-node": "^10.9.1", @@ -46,6 +48,7 @@ "node": ">=18.0.0" }, "nodemonConfig": { + "ext": "json,ts", "watch": [ ".env", "src" diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 974caed55ed5..19598b6d9f66 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -1,8 +1,8 @@ -import express from "express"; +import express, { Request, Response } from "express"; import { Router } from "express"; import compression from "compression"; -import { Origin, origin } from "./env.js"; +import { Origin } from "./env.js"; import { createContentProxy } from "./handlers/content.js"; import { proxyKevel } from "./handlers/kevel.js"; import { proxyRumba } from "./handlers/rumba.js"; @@ -15,22 +15,41 @@ import { redirectMovedPages } from "./middlewares/redirectMovedPages.js"; import { redirectFundamental } from "./middlewares/redirectFundamental.js"; import { redirectLocale } from "./middlewares/redirectLocale.js"; import { redirectTrailingSlash } from "./middlewares/redirectTrailingSlash.js"; +import { requireOrigin } from "./middlewares/requireOrigin.js"; -const mainRouter = Router(); const proxyContent = createContentProxy(); -mainRouter.use(redirectLeadingSlash); -mainRouter.all("/api/v1/stripe/plans", plans); -mainRouter.all("/api/*", proxyRumba); -mainRouter.all("/admin-api/*", proxyRumba); -mainRouter.all("/events/fxa/*", proxyRumba); -mainRouter.all("/users/fxa/*", proxyRumba); -mainRouter.all("/submit/mdn-yari/*", compression(), proxyTelemetry); -mainRouter.all("/pong/*", express.json(), proxyKevel); -mainRouter.all("/pimg/*", proxyKevel); -mainRouter.get("/sitemaps/*", proxyContent); -mainRouter.get("/static/*", proxyContent); -mainRouter.get( + +const router = Router(); +router.use(redirectLeadingSlash); +router.all("/api/v1/stripe/plans", requireOrigin(Origin.main), plans); +router.all("/api/*", requireOrigin(Origin.main), proxyRumba); +router.all("/admin-api/*", requireOrigin(Origin.main), proxyRumba); +router.all("/events/fxa/*", requireOrigin(Origin.main), proxyRumba); +router.all("/users/fxa/*", requireOrigin(Origin.main), proxyRumba); +router.all( + "/submit/mdn-yari/*", + requireOrigin(Origin.main), + compression(), + proxyTelemetry +); +router.all("/pong/*", requireOrigin(Origin.main), express.json(), proxyKevel); +router.all("/pimg/*", requireOrigin(Origin.main), proxyKevel); +router.get("/sitemaps/*", requireOrigin(Origin.main), proxyContent); +router.get("/static/*", requireOrigin(Origin.main), proxyContent); +router.get( + "/[^/]+/docs/*/_sample_.*.html", + requireOrigin(Origin.liveSamples), + pathnameLC, + proxyContent +); +router.get( + "/[^/]+/docs/*/*.(png|jpeg|jpg|gif|svg|webp)", + requireOrigin(Origin.main, Origin.liveSamples), + proxyContent +); +router.get( "/[^/]+/docs/*", + requireOrigin(Origin.main), redirectFundamental, redirectLocale, redirectTrailingSlash, @@ -38,9 +57,15 @@ mainRouter.get( resolveIndexHTML, proxyContent ); -mainRouter.get("/[^/]+/search-index.json", pathnameLC, proxyContent); -mainRouter.get( +router.get( + "/[^/]+/search-index.json", + requireOrigin(Origin.main), + pathnameLC, + proxyContent +); +router.get( "*", + requireOrigin(Origin.main), redirectFundamental, redirectLocale, redirectTrailingSlash, @@ -48,33 +73,9 @@ mainRouter.get( proxyContent ); -const liveSampleRouter = Router(); -liveSampleRouter.use(pathnameLC); -liveSampleRouter.get("/[^/]+/docs/*/_sample_.*.html", proxyContent); -liveSampleRouter.get( - "/[^/]+/docs/*/*.(png|jpeg|jpg|gif|svg|webp)", - proxyContent -); -liveSampleRouter.get("*", (_req: express.Request, res: express.Response) => - res.status(404).send() -); - -export function createHandler(o?: Origin) { - return async ( - req: express.Request, - res: express.Response, - next: express.NextFunction = () => { +export function createHandler() { + return async (req: Request, res: Response) => + router(req, res, () => { /* noop */ - } - ) => { - const rPath = req.path; - const reqOrigin = o || origin(req); - if (reqOrigin === Origin.main && !rPath.includes("/_sample_.")) { - return mainRouter(req, res, next); - } else if (reqOrigin === Origin.liveSamples) { - return liveSampleRouter(req, res, next); - } else { - return res.status(404).send(); - } - }; + }); } diff --git a/gcp/function/src/cli.ts b/gcp/function/src/cli.ts deleted file mode 100644 index 5def0baf58ae..000000000000 --- a/gcp/function/src/cli.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { readFileSync } from "node:fs"; -import { createServer as createHttpsServer } from "node:https"; - -import express from "express"; - -import { createHandler } from "./app.js"; -import { - DEBUG_TELEMETRY, - HTTPS_CERT_FILE, - HTTPS_KEY_FILE, - Origin, -} from "./env.js"; - -const contentApp = express(); -const contentPort = 3000; - -contentApp.all("*", createHandler(Origin.main)); -contentApp.listen(contentPort, () => { - console.log(`Content app listening on port ${contentPort}`); -}); - -if (HTTPS_CERT_FILE && HTTPS_KEY_FILE) { - const PORT = 443; - createHttpsServer( - { - key: readFileSync(HTTPS_KEY_FILE), - cert: readFileSync(HTTPS_CERT_FILE), - }, - contentApp - ).listen(PORT, () => - console.log(`Content app listening on port ${PORT} [HTTPS]`) - ); -} - -const liveSampleApp = express(); -const liveSamplePort = 5042; - -liveSampleApp.all("*", createHandler(Origin.liveSamples)); -liveSampleApp.listen(liveSamplePort, () => { - console.log(`Sample app listening on port ${liveSamplePort}`); -}); - -if (DEBUG_TELEMETRY) { - const debugApp = express(); - debugApp.all("*", async (req: express.Request, res: express.Response) => { - const { method, url, query, headers } = req; - const payload = { - method, - url, - headers, - query, - }; - res.setHeader("Content-Type", "application/json"), - res.write(JSON.stringify(payload)); - res.end(); - }); - debugApp.listen(8888); -} diff --git a/gcp/function/src/env.ts b/gcp/function/src/env.ts index 55a1b1395022..1cc02bbb4b33 100644 --- a/gcp/function/src/env.ts +++ b/gcp/function/src/env.ts @@ -42,6 +42,16 @@ export const SOURCE_LIVE_SAMPLES: string = export const SOURCE_RUMBA: string = process.env["SOURCE_RUMBA"] || "https://developer.mozilla.org"; +export function getOriginFromRequest(req: express.Request): Origin { + if (req.hostname === ORIGIN_MAIN && !req.path.includes("/_sample_.")) { + return Origin.main; + } else if (req.hostname === ORIGIN_LIVE_SAMPLES) { + return Origin.liveSamples; + } else { + return Origin.unsafe; + } +} + export function sourceUri(source: Source): string { switch (source) { case Source.content: diff --git a/gcp/function/src/middlewares/requireOrigin.ts b/gcp/function/src/middlewares/requireOrigin.ts new file mode 100644 index 000000000000..91f29d12c49f --- /dev/null +++ b/gcp/function/src/middlewares/requireOrigin.ts @@ -0,0 +1,14 @@ +import type { NextFunction, Request, Response } from "express"; +import { Origin, getOriginFromRequest } from "../env.js"; + +export function requireOrigin(...expectedOrigins: Origin[]) { + return async (req: Request, res: Response, next: NextFunction) => { + const actualOrigin = getOriginFromRequest(req); + + if (expectedOrigins.includes(actualOrigin)) { + return next(); + } else { + return res.status(404).end(); + } + }; +} diff --git a/gcp/function/src/proxy.ts b/gcp/function/src/proxy.ts new file mode 100644 index 000000000000..0a7416963017 --- /dev/null +++ b/gcp/function/src/proxy.ts @@ -0,0 +1,45 @@ +import { readFileSync } from "node:fs"; +import { createServer } from "node:https"; +import httpProxy from "http-proxy"; + +import { HTTPS_CERT_FILE, HTTPS_KEY_FILE } from "./env.js"; + +if (HTTPS_CERT_FILE && HTTPS_KEY_FILE) { + const proxy = httpProxy.createProxyServer({ + target: "http://localhost:5100", + }); + + const server = createServer( + { + key: readFileSync(HTTPS_KEY_FILE), + cert: readFileSync(HTTPS_CERT_FILE), + }, + (req, res) => proxy.web(req, res) + ); + + proxy.on("error", (err) => { + console.error("Proxy error:", err); + + // Restart the server + server.close(function () { + console.log("Server closed"); + server.listen(443, function () { + console.log("Server restarted"); + }); + }); + }); + + server.listen(443, () => console.log(`HTTPS proxy running on port 443`)); +} else { + console.log("HTTPS proxy disabled!"); + console.log( + "Note: Set HTTPS_CERT_FILE and HTTPS_KEY_FILE in .env to enable it." + ); + console.log( + "Hint: Use mkcert to create a locally-trusted development certificate." + ); + // eslint-disable-next-line no-constant-condition + while (true) { + // Nothing. + } +} diff --git a/gcp/function/tsconfig.json b/gcp/function/tsconfig.json index 3e1c5270f1ea..f513a54ad476 100644 --- a/gcp/function/tsconfig.json +++ b/gcp/function/tsconfig.json @@ -20,7 +20,6 @@ "noUncheckedIndexedAccess": true, "noUnusedLocals": true, "noUnusedParameters": true, - "importsNotUsedAsValues": "error", "checkJs": true }, "ts-node": { From b55c50a5b6e8ba2dc6ebabd8c4c703ce1e70df7e Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 16:59:59 +0200 Subject: [PATCH 202/343] fixup! feat(gcp/function): use @google-cloud/functions-framework --- gcp/function/.gcloudignore | 1 + 1 file changed, 1 insertion(+) diff --git a/gcp/function/.gcloudignore b/gcp/function/.gcloudignore index aafa4b335a32..3d84c864776a 100644 --- a/gcp/function/.gcloudignore +++ b/gcp/function/.gcloudignore @@ -16,3 +16,4 @@ #!include:.gitignore !redirects.json +!src/internal/*.js From f4158dddb0f46d61af89e8e819d6e6b739db8e46 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 17:06:57 +0200 Subject: [PATCH 203/343] fixup! fixup! feat(gcp/function): use @google-cloud/functions-framework --- gcp/function/.gcloudignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/function/.gcloudignore b/gcp/function/.gcloudignore index 3d84c864776a..2c8745fc710b 100644 --- a/gcp/function/.gcloudignore +++ b/gcp/function/.gcloudignore @@ -16,4 +16,4 @@ #!include:.gitignore !redirects.json -!src/internal/*.js +!src/internal/ From 8441e2291a13234eabf979005d9e695a0bafb8f6 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 17:07:39 +0200 Subject: [PATCH 204/343] fix(gcp/function): add DOM to lib --- gcp/function/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/function/tsconfig.json b/gcp/function/tsconfig.json index f513a54ad476..fae5d6bfe79b 100644 --- a/gcp/function/tsconfig.json +++ b/gcp/function/tsconfig.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/tsconfig", "display": "Node 18 + ESM + Strictest", "compilerOptions": { - "lib": ["es2022"], + "lib": ["es2022", "DOM"], "module": "es2022", "target": "es2022", "strict": true, From 3e28a021fe9935d20bd2d4205fb3d4d3327f3d9a Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 17:23:57 +0200 Subject: [PATCH 205/343] fix(gcp/function): reference @yari-internal directly --- gcp/function/src/build.ts | 2 +- gcp/function/src/headers.ts | 2 +- gcp/function/src/middlewares/redirectFundamental.ts | 2 +- gcp/function/src/middlewares/redirectLocale.ts | 4 ++-- gcp/function/src/middlewares/redirectMovedPages.ts | 2 +- gcp/function/src/middlewares/redirectTrailingSlash.ts | 2 +- gcp/function/src/middlewares/resolveIndexHTML.ts | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/gcp/function/src/build.ts b/gcp/function/src/build.ts index f0f757cde33a..5135513d4576 100644 --- a/gcp/function/src/build.ts +++ b/gcp/function/src/build.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url"; import dotenv from "dotenv"; -import { VALID_LOCALES } from "@yari-internal/constants"; +import { VALID_LOCALES } from "./internal/constants/index.js"; const dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/gcp/function/src/headers.ts b/gcp/function/src/headers.ts index 728bde1d615e..29bba1cde6ef 100644 --- a/gcp/function/src/headers.ts +++ b/gcp/function/src/headers.ts @@ -1,7 +1,7 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import type express from "express"; -import { CSP_VALUE } from "@yari-internal/constants"; +import { CSP_VALUE } from "./internal/constants/index.js"; export function withProxyResponseHeaders( _proxyRes: IncomingMessage, diff --git a/gcp/function/src/middlewares/redirectFundamental.ts b/gcp/function/src/middlewares/redirectFundamental.ts index 83f1eeff2498..d5f9d62f8cac 100644 --- a/gcp/function/src/middlewares/redirectFundamental.ts +++ b/gcp/function/src/middlewares/redirectFundamental.ts @@ -1,7 +1,7 @@ import type express from "express"; import { THIRTY_DAYS } from "../constants.js"; -import { resolveFundamental } from "@yari-internal/fundamental-redirects"; +import { resolveFundamental } from "internal/fundamental-redirects/index.js"; import { redirect } from "../utils.js"; export function redirectFundamental( diff --git a/gcp/function/src/middlewares/redirectLocale.ts b/gcp/function/src/middlewares/redirectLocale.ts index 83aab399ed74..0d02a559c09c 100644 --- a/gcp/function/src/middlewares/redirectLocale.ts +++ b/gcp/function/src/middlewares/redirectLocale.ts @@ -1,7 +1,7 @@ import type express from "express"; -import { getLocale } from "@yari-internal/locale-utils"; -import { VALID_LOCALES } from "@yari-internal/constants"; +import { getLocale } from "../internal/locale-utils/index.js"; +import { VALID_LOCALES } from "../internal/constants/index.js"; import { redirect } from "../utils.js"; const NEEDS_LOCALE = /^\/(?:docs|search|settings|signin|signup|plus)(?:$|\/)/; diff --git a/gcp/function/src/middlewares/redirectMovedPages.ts b/gcp/function/src/middlewares/redirectMovedPages.ts index 9d85b11d2df0..5aca37df6e48 100644 --- a/gcp/function/src/middlewares/redirectMovedPages.ts +++ b/gcp/function/src/middlewares/redirectMovedPages.ts @@ -1,7 +1,7 @@ import { createRequire } from "node:module"; import type express from "express"; -import { decodePath } from "@yari-internal/slug-utils"; +import { decodePath } from "../internal/slug-utils/index.js"; import { THIRTY_DAYS } from "../constants.js"; import { redirect } from "../utils.js"; diff --git a/gcp/function/src/middlewares/redirectTrailingSlash.ts b/gcp/function/src/middlewares/redirectTrailingSlash.ts index 660130ef9497..113b45f075ee 100644 --- a/gcp/function/src/middlewares/redirectTrailingSlash.ts +++ b/gcp/function/src/middlewares/redirectTrailingSlash.ts @@ -1,7 +1,7 @@ import type express from "express"; import { THIRTY_DAYS } from "../constants.js"; -import { VALID_LOCALES } from "@yari-internal/constants"; +import { VALID_LOCALES } from "../internal/constants/index.js"; import { redirect } from "../utils.js"; // Note that the keys of "VALID_LOCALES" are lowercase locales. diff --git a/gcp/function/src/middlewares/resolveIndexHTML.ts b/gcp/function/src/middlewares/resolveIndexHTML.ts index 35d2c39e6ec3..85cd526d6ba3 100644 --- a/gcp/function/src/middlewares/resolveIndexHTML.ts +++ b/gcp/function/src/middlewares/resolveIndexHTML.ts @@ -1,5 +1,5 @@ import type express from "express"; -import { slugToFolder } from "@yari-internal/slug-utils"; +import { slugToFolder } from "../internal/slug-utils/index.js"; import * as path from "node:path"; export function resolveIndexHTML( From 88db32b4be806c2d336381adf4e402c83de1e43c Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 17:32:05 +0200 Subject: [PATCH 206/343] fixup! fix(gcp/function): reference @yari-internal directly --- gcp/function/src/middlewares/redirectFundamental.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/function/src/middlewares/redirectFundamental.ts b/gcp/function/src/middlewares/redirectFundamental.ts index d5f9d62f8cac..06c8e647a5f6 100644 --- a/gcp/function/src/middlewares/redirectFundamental.ts +++ b/gcp/function/src/middlewares/redirectFundamental.ts @@ -1,7 +1,7 @@ import type express from "express"; import { THIRTY_DAYS } from "../constants.js"; -import { resolveFundamental } from "internal/fundamental-redirects/index.js"; +import { resolveFundamental } from "../internal/fundamental-redirects/index.js"; import { redirect } from "../utils.js"; export function redirectFundamental( From 29c65bd9be10f4e5e434bf83dbd33eafd816b8c6 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 17:38:40 +0200 Subject: [PATCH 207/343] fixup! fixup! fixup! feat(gcp/function): use @google-cloud/functions-framework --- gcp/function/.gcloudignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/function/.gcloudignore b/gcp/function/.gcloudignore index 2c8745fc710b..79b725a2ebad 100644 --- a/gcp/function/.gcloudignore +++ b/gcp/function/.gcloudignore @@ -16,4 +16,4 @@ #!include:.gitignore !redirects.json -!src/internal/ +!src/internal/** From 7a297453bc602ee9ccbf37fa423cea5d0f4702e3 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 17:40:18 +0200 Subject: [PATCH 208/343] chore(gcp/function): generate redirects map on demand only Build is now part of the deployment. --- .github/workflows/xyz-build.yml | 5 ++--- gcp/function/package.json | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 7291315320ba..6fab503a801b 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -179,15 +179,14 @@ jobs: gsutil -q -m cp -r client/build/static gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH/static gsutil -q -m rsync -cdrj html,json,txt client/build gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH - - name: Build Function + - name: Generate redirects map if: ${{ ! vars.SKIP_FUNCTION }} working-directory: gcp/function env: CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files run: | - npm ci - npm run build + npm run redirects - name: Deploy Function if: ${{ ! vars.SKIP_FUNCTION }} diff --git a/gcp/function/package.json b/gcp/function/package.json index 1ea2f1a372b3..835aa18d0862 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -11,7 +11,7 @@ "build": "tsc -b", "copy-internal": "rm -rf ./src/internal && cp -R ../../libs ./src/internal", "gcp-build": "npm run build", - "prepare": "([ ! -e ../../libs ] || npm run copy-internal) && ([ -e redirects.json ] || npm run redirects)", + "prepare": "([ ! -e ../../libs ] || npm run copy-internal)", "proxy": "ts-node src/proxy.ts", "redirects": "ts-node src/build.ts", "server": "npm run build && functions-framework --target=mdnHandler", From 3c8734bb148f2da53b4f5465cb28de9067fb8447 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 17:54:07 +0200 Subject: [PATCH 209/343] feat(gcp/function): deploy functions concurrently in all regions --- .github/workflows/xyz-build.yml | 14 ++++++++++++-- gcp/function/.gitignore | 1 + 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 6fab503a801b..3fbf89b0cf8a 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -216,10 +216,20 @@ jobs: --set-secrets="KEVEL_NETWORK_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-network-id/versions/latest" \ --set-secrets="SIGN_SECRET=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-sign-secret/versions/latest" \ --set-secrets="CARBON_ZONE_KEY=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-carbon-zone-key/versions/latest" \ - --set-secrets="CARBON_FALLBACK_ENABLED=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-fallback-enabled/versions/latest" - \ + --set-secrets="CARBON_FALLBACK_ENABLED=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-fallback-enabled/versions/latest" \ + & + pids+=($!) + done + + tail -n 0 -qF *.log & + tail=$! + + for pid in "${pids[@]}"; do + wait $pid done + kill $tail + - name: Invalidate CDN if: ${{ ! vars.SKIP_INVALIDATE }} run: |- diff --git a/gcp/function/.gitignore b/gcp/function/.gitignore index a8c548947e16..9f83d01e07ab 100644 --- a/gcp/function/.gitignore +++ b/gcp/function/.gitignore @@ -1,5 +1,6 @@ node_modules .env* +*.log redirects.json src/**/*.js src/internal From de14cafaee77e73684714a757bb70c4b8008c130 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 19:01:48 +0200 Subject: [PATCH 210/343] ci(xyz-build): redirect build into log files --- .github/workflows/xyz-build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 3fbf89b0cf8a..64597ab51fdb 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -145,11 +145,11 @@ jobs: yarn tool sync-translated-content for locale in en-us es fr ja ko pt-br ru zh-cn zh-tw; do - yarn build --locale $locale & + yarn build --locale $locale > build-$locale.log & pids+=($!) done - tail -n 0 -qF *.log & + tail -n 0 -qF build-*.log & tail=$! for pid in "${pids[@]}"; do @@ -217,11 +217,11 @@ jobs: --set-secrets="SIGN_SECRET=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-sign-secret/versions/latest" \ --set-secrets="CARBON_ZONE_KEY=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-carbon-zone-key/versions/latest" \ --set-secrets="CARBON_FALLBACK_ENABLED=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-fallback-enabled/versions/latest" \ - & + > deploy-$region.log & pids+=($!) done - tail -n 0 -qF *.log & + tail -n 0 -qF deploy-*.log & tail=$! for pid in "${pids[@]}"; do From 199305851230ad44e8cc83936a596872fbbfbcd4 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 7 Apr 2023 19:14:24 +0200 Subject: [PATCH 211/343] ci(xyz-build): print tail headers for function deploy --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 64597ab51fdb..88b5c74eb8f5 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -221,7 +221,7 @@ jobs: pids+=($!) done - tail -n 0 -qF deploy-*.log & + tail -n 0 -F deploy-*.log & tail=$! for pid in "${pids[@]}"; do From de619883030748847f5479904762cbbb8bfe2006 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 11 Apr 2023 13:03:35 +0200 Subject: [PATCH 212/343] ci(stage-build): add SKIP conditions --- .github/workflows/stage-build.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index d073bb231a82..3062364a725c 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -94,6 +94,7 @@ jobs: fetch-depth: 0 - uses: actions/checkout@v3 + if: ${{ ! vars.SKIP_BUILD }} with: repository: mdn/mdn-contributor-spotlight path: mdn/mdn-contributor-spotlight @@ -108,19 +109,23 @@ jobs: run: yarn --frozen-lockfile - name: Install Python + if: ${{ ! vars.SKIP_BUILD }} uses: actions/setup-python@v4 with: python-version: "3.10" - name: Install Python poetry + if: ${{ ! vars.SKIP_BUILD }} uses: snok/install-poetry@v1 - name: Install deployer + if: ${{ ! vars.SKIP_BUILD }} run: | cd deployer poetry install - name: Display Python & Poetry version + if: ${{ ! vars.SKIP_BUILD }} run: | python --version poetry --version @@ -135,6 +140,7 @@ jobs: run: cat /proc/cpuinfo - name: Build everything + if: ${{ ! vars.SKIP_BUILD }} env: # Remember, the mdn/content repo got cloned into `pwd` into a # sub-folder called "mdn/content" @@ -230,6 +236,7 @@ jobs: yarn build --sitemap-index - name: Deploy with deployer + if: ${{ ! vars.SKIP_BUILD }} env: GITHUB_SHA: ${{ env.GITHUB_SHA }} GITHUB_RUN_ID: ${{ env.GITHUB_RUN_ID }} @@ -272,6 +279,7 @@ jobs: poetry run deployer search-index ../client/build - name: Configure AWS Credentials + if: ${{ ! vars.SKIP_INVALIDATE }} uses: aws-actions/configure-aws-credentials@v1-node16 with: aws-access-key-id: ${{ secrets.DEPLOYER_STAGE_AND_DEV_AWS_ACCESS_KEY_ID }} @@ -279,12 +287,14 @@ jobs: aws-region: us-east-1 - name: Invalidate CDN + if: ${{ ! vars.SKIP_INVALIDATE }} env: DISTRIBUTION: E2MLRMA1VTVDHX PATHS: /* run: aws cloudfront create-invalidation --distribution-id "$DISTRIBUTION" --paths "$PATHS" - name: Authenticate with GCP + if: ${{ ! vars.SKIP_BUILD }} uses: google-github-actions/auth@v1 with: token_format: access_token @@ -292,14 +302,17 @@ jobs: workload_identity_provider: projects/${{ secrets.WIP_PROJECT_ID }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions - name: Setup gcloud + if: ${{ ! vars.SKIP_BUILD }} uses: google-github-actions/setup-gcloud@v1 - name: Sync Yari Content + if: ${{ ! vars.SKIP_BUILD }} run: |- gsutil -m -h "Cache-Control:public, max-age=86400" rsync -crj html,json,txt client/build gs://content-stage-mdn/main # Deploy Function - name: Authenticate with GCP + if: ${{ ! vars.SKIP_FUNCTION || !vars.SKIP_INVALIDATE }} uses: google-github-actions/auth@v1 with: token_format: access_token @@ -307,9 +320,11 @@ jobs: workload_identity_provider: projects/${{ secrets.WIP_PROJECT_ID }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions - name: Setup gcloud + if: ${{ ! vars.SKIP_FUNCTION || !vars.SKIP_INVALIDATE }} uses: google-github-actions/setup-gcloud@v1 - name: Build Function + if: ${{ ! vars.SKIP_FUNCTION }} working-directory: gcp/function env: CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files @@ -319,6 +334,7 @@ jobs: npm run build - name: Deploy Function + if: ${{ ! vars.SKIP_FUNCTION }} run: |- for region in europe-west1 us-west1 asia-east1; do gcloud functions deploy mdn-nonprod-stage-$region \ From 5425885081256311e9f00950e3a9159ef1801356 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 11 Apr 2023 13:09:39 +0200 Subject: [PATCH 213/343] ci(stage-build): invalidate cdn cache in GCP --- .github/workflows/stage-build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 3062364a725c..016a2623ee06 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -359,6 +359,11 @@ jobs: \ done + - name: Invalidate CDN + if: ${{ ! vars.SKIP_INVALIDATE }} + run: |- + gcloud compute url-maps invalidate-cdn-cache ${{ vars.GCP_LOAD_BALANCER_NAME }} --path "/*" + - name: Slack Notification if: failure() uses: rtCamp/action-slack-notify@v2 From f44fed503aeb41b173c608ed2912045da326c240 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 11 Apr 2023 13:11:12 +0200 Subject: [PATCH 214/343] ci(xyz-build): only run on function changes --- .github/workflows/xyz-build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 88b5c74eb8f5..9bd6249cf326 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -4,6 +4,9 @@ on: push: branches: - gcp + paths: + - .github/workflows/xyz-build.yml + - gcp/function/** workflow_dispatch: From 0b61f74d59184bf16602d833d6f170bde48ee624 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 11 Apr 2023 13:12:03 +0200 Subject: [PATCH 215/343] chore: empty commit To see if it triggers the xyz-build, which it should no more. From ee6535cea8515844161dae2ec42ec4a5e23138d3 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 11 Apr 2023 13:20:07 +0200 Subject: [PATCH 216/343] fixup! ci(stage-build): invalidate cdn cache in GCP --- .github/workflows/stage-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 016a2623ee06..66fed08bf0bc 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -106,6 +106,7 @@ jobs: cache: yarn - name: Install all yarn packages + if: ${{ ! vars.SKIP_BUILD || ! vars.SKIP_FUNCTION }} run: yarn --frozen-lockfile - name: Install Python From 82d24048b8085df760abd55ed159a49701f0c9d2 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 11 Apr 2023 13:27:22 +0200 Subject: [PATCH 217/343] ci(stage-build): GCP_LOAD_BALANCER_NAME is a secret --- .github/workflows/stage-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 66fed08bf0bc..dd5194ae4003 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -363,7 +363,7 @@ jobs: - name: Invalidate CDN if: ${{ ! vars.SKIP_INVALIDATE }} run: |- - gcloud compute url-maps invalidate-cdn-cache ${{ vars.GCP_LOAD_BALANCER_NAME }} --path "/*" + gcloud compute url-maps invalidate-cdn-cache ${{ secrets.GCP_LOAD_BALANCER_NAME }} --path "/*" - name: Slack Notification if: failure() From 28d0bf88dffdf3697377a0d8187fce03154afaf7 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 11 Apr 2023 13:28:51 +0200 Subject: [PATCH 218/343] fixup! ci(stage-build): add SKIP conditions --- .github/workflows/stage-build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index dd5194ae4003..7e0db52a48d6 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -65,6 +65,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/checkout@v3 + if: ${{ ! vars.SKIP_BUILD || ! vars.SKIP_FUNCTION }} with: repository: mdn/content path: mdn/content @@ -87,6 +88,7 @@ jobs: echo "DEPLOYER_LOG_EACH_SUCCESSFUL_UPLOAD=${{ github.event.inputs.log_each_successful_upload || env.DEFAULT_LOG_EACH_SUCCESSFUL_UPLOAD }}" >> $GITHUB_ENV - uses: actions/checkout@v3 + if: ${{ ! vars.SKIP_BUILD || ! vars.SKIP_FUNCTION }} with: repository: mdn/translated-content path: mdn/translated-content @@ -100,6 +102,7 @@ jobs: path: mdn/mdn-contributor-spotlight - name: Setup Node.js environment + if: ${{ ! vars.SKIP_BUILD || ! vars.SKIP_FUNCTION }} uses: actions/setup-node@v3 with: node-version: 18 From 4856f5d230b9b69ffc68d53d6658190f31454d19 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 11 Apr 2023 13:51:19 +0200 Subject: [PATCH 219/343] ci(stage-build): generate redirects map --- .github/workflows/stage-build.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 7e0db52a48d6..d62158deb2b1 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -327,15 +327,14 @@ jobs: if: ${{ ! vars.SKIP_FUNCTION || !vars.SKIP_INVALIDATE }} uses: google-github-actions/setup-gcloud@v1 - - name: Build Function + - name: Generate redirects map if: ${{ ! vars.SKIP_FUNCTION }} working-directory: gcp/function env: CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files run: | - npm ci - npm run build + npm run redirects - name: Deploy Function if: ${{ ! vars.SKIP_FUNCTION }} From ed4fef1ef7b338bccf400910b8e9c19efa48cd5e Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 11 Apr 2023 14:00:14 +0200 Subject: [PATCH 220/343] ci(stage-build): deploy Function concurrently --- .github/workflows/stage-build.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index d62158deb2b1..71655556907c 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -358,10 +358,20 @@ jobs: --set-secrets="KEVEL_NETWORK_ID=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-kevel-network-id/versions/latest" \ --set-secrets="SIGN_SECRET=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-sign-secret/versions/latest" \ --set-secrets="CARBON_ZONE_KEY=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-carbon-zone-key/versions/latest" \ - --set-secrets="CARBON_FALLBACK_ENABLED=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-fallback-enabled/versions/latest" - \ + --set-secrets="CARBON_FALLBACK_ENABLED=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-fallback-enabled/versions/latest" \ + > deploy-$region.log & + pids+=($!) done + tail -n 0 -F deploy-*.log & + tail=$! + + for pid in "${pids[@]}"; do + wait $pid + done + + kill $tail + - name: Invalidate CDN if: ${{ ! vars.SKIP_INVALIDATE }} run: |- From bc410d53168c1987499eec5f6cc9952b16c71323 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 11 Apr 2023 14:10:40 +0200 Subject: [PATCH 221/343] chore(workflows): prefix function build output --- .github/workflows/stage-build.yml | 7 +------ .github/workflows/xyz-build.yml | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 71655556907c..0cdac3278c4c 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -359,19 +359,14 @@ jobs: --set-secrets="SIGN_SECRET=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-sign-secret/versions/latest" \ --set-secrets="CARBON_ZONE_KEY=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-carbon-zone-key/versions/latest" \ --set-secrets="CARBON_FALLBACK_ENABLED=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-fallback-enabled/versions/latest" \ - > deploy-$region.log & + | sed "s/^/[$region] /" & pids+=($!) done - tail -n 0 -F deploy-*.log & - tail=$! - for pid in "${pids[@]}"; do wait $pid done - kill $tail - - name: Invalidate CDN if: ${{ ! vars.SKIP_INVALIDATE }} run: |- diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 9bd6249cf326..80dae1851bd4 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -220,19 +220,14 @@ jobs: --set-secrets="SIGN_SECRET=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-sign-secret/versions/latest" \ --set-secrets="CARBON_ZONE_KEY=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-carbon-zone-key/versions/latest" \ --set-secrets="CARBON_FALLBACK_ENABLED=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-fallback-enabled/versions/latest" \ - > deploy-$region.log & + | sed "s/^/[$region] /" & pids+=($!) done - tail -n 0 -F deploy-*.log & - tail=$! - for pid in "${pids[@]}"; do wait $pid done - kill $tail - - name: Invalidate CDN if: ${{ ! vars.SKIP_INVALIDATE }} run: |- From 66a6a033092e81129b35b73ff8ca1a365a679a36 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 11 Apr 2023 14:44:41 +0200 Subject: [PATCH 222/343] ci(xyz-build): use rumba in GCP --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 80dae1851bd4..38117deef381 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -210,7 +210,7 @@ jobs: --set-env-vars="ORIGIN_MAIN=${{ vars.ORIGIN_MAIN }}" \ --set-env-vars="ORIGIN_LIVE_SAMPLES=${{ vars.ORIGIN_LIVE_SAMPLES }}" \ --set-env-vars="SOURCE_CONTENT=https://storage.googleapis.com/${{ vars.GCP_BUCKET_NAME }}/${{ env.BUCKET_PATH }}/" \ - --set-env-vars="SOURCE_RUMBA=https://developer.allizom.org/" \ + --set-env-vars="SOURCE_RUMBA=https://api.developer.allizom.org/" \ --set-env-vars="SENTRY_DSN=${{ secrets.SENTRY_DSN_CLOUD_FUNCTION }}" \ --set-env-vars="SENTRY_ENVIRONMENT=xyz" \ --set-env-vars="SENTRY_TRACES_SAMPLE_RATE=${{ vars.SENTRY_TRACES_SAMPLE_RATE }}" \ From a33825126bc6e98dfb771232193cb0c8fd602ff0 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 11 Apr 2023 18:15:23 +0200 Subject: [PATCH 223/343] ci(stage-build): support skipping AWS --- .github/workflows/stage-build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 0cdac3278c4c..6d3271fa432b 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -240,7 +240,7 @@ jobs: yarn build --sitemap-index - name: Deploy with deployer - if: ${{ ! vars.SKIP_BUILD }} + if: ${{ ! (vars.SKIP_BUILD || vars.SKIP_AWS) }} env: GITHUB_SHA: ${{ env.GITHUB_SHA }} GITHUB_RUN_ID: ${{ env.GITHUB_RUN_ID }} @@ -283,7 +283,7 @@ jobs: poetry run deployer search-index ../client/build - name: Configure AWS Credentials - if: ${{ ! vars.SKIP_INVALIDATE }} + if: ${{ ! (vars.SKIP_INVALIDATE || vars.SKIP_AWS) }} uses: aws-actions/configure-aws-credentials@v1-node16 with: aws-access-key-id: ${{ secrets.DEPLOYER_STAGE_AND_DEV_AWS_ACCESS_KEY_ID }} @@ -291,7 +291,7 @@ jobs: aws-region: us-east-1 - name: Invalidate CDN - if: ${{ ! vars.SKIP_INVALIDATE }} + if: ${{ ! (vars.SKIP_INVALIDATE || vars.SKIP_AWS) }} env: DISTRIBUTION: E2MLRMA1VTVDHX PATHS: /* From 3cc46dcbfee2565579f3bcfb08289e5a74b72b33 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 11 Apr 2023 18:32:21 +0200 Subject: [PATCH 224/343] ci(workflows): prefix stderr as well --- .github/workflows/stage-build.yml | 2 +- .github/workflows/xyz-build.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 6d3271fa432b..3431e5cdae91 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -359,7 +359,7 @@ jobs: --set-secrets="SIGN_SECRET=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-sign-secret/versions/latest" \ --set-secrets="CARBON_ZONE_KEY=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-carbon-zone-key/versions/latest" \ --set-secrets="CARBON_FALLBACK_ENABLED=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-fallback-enabled/versions/latest" \ - | sed "s/^/[$region] /" & + 2>&1 | sed "s/^/[$region] /" & pids+=($!) done diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 38117deef381..8dde1c9d2f38 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -220,7 +220,7 @@ jobs: --set-secrets="SIGN_SECRET=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-sign-secret/versions/latest" \ --set-secrets="CARBON_ZONE_KEY=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-carbon-zone-key/versions/latest" \ --set-secrets="CARBON_FALLBACK_ENABLED=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-fallback-enabled/versions/latest" \ - | sed "s/^/[$region] /" & + 2>&1 | sed "s/^/[$region] /" & pids+=($!) done From 91c70f47a9e55f9333e2b8ecdfb0282fecdd9140 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 11 Apr 2023 18:35:09 +0200 Subject: [PATCH 225/343] ci(xyz-build): prefix build logs with sed --- .github/workflows/xyz-build.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 8dde1c9d2f38..100bc86ae8e4 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -148,19 +148,14 @@ jobs: yarn tool sync-translated-content for locale in en-us es fr ja ko pt-br ru zh-cn zh-tw; do - yarn build --locale $locale > build-$locale.log & + yarn build --locale $locale 2>&1 | sed "s/^/[$locale] /" & pids+=($!) done - tail -n 0 -qF build-*.log & - tail=$! - for pid in "${pids[@]}"; do wait $pid done - kill $tail - du -sh client/build # Generate sitemap index file From 33db6869bca5df441377ff44562f5353195681ea Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 11 Apr 2023 18:38:40 +0200 Subject: [PATCH 226/343] ci(stage-build): add sentry env vars --- .github/workflows/stage-build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 3431e5cdae91..fec294dfe0dc 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -354,6 +354,9 @@ jobs: --set-env-vars="ORIGIN_LIVE_SAMPLES=yari-demos.developer.allizom.org" \ --set-env-vars="SOURCE_CONTENT=https://storage.googleapis.com/content-stage-mdn/main/" \ --set-env-vars="SOURCE_RUMBA=https://api.developer.allizom.org/" \ + --set-env-vars="SENTRY_DSN=${{ secrets.SENTRY_DSN_CLOUD_FUNCTION }}" \ + --set-env-vars="SENTRY_ENVIRONMENT=stage" \ + --set-env-vars="SENTRY_RELEASE=${{ github.sha }}" \ --set-secrets="KEVEL_SITE_ID=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-kevel-site-id/versions/latest" \ --set-secrets="KEVEL_NETWORK_ID=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-kevel-network-id/versions/latest" \ --set-secrets="SIGN_SECRET=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-sign-secret/versions/latest" \ From caa03f98b383b4f4dd1470cf61d17740be301dd3 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 11 Apr 2023 23:12:53 +0200 Subject: [PATCH 227/343] fix(gcp/function): use fixRequestBody for content/rumba as well --- gcp/function/src/handlers/content.ts | 2 ++ gcp/function/src/handlers/rumba.ts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/gcp/function/src/handlers/content.ts b/gcp/function/src/handlers/content.ts index 2e8c3eaa1056..2af5a7474b08 100644 --- a/gcp/function/src/handlers/content.ts +++ b/gcp/function/src/handlers/content.ts @@ -1,6 +1,7 @@ import type express from "express"; import { createProxyMiddleware, + fixRequestBody, responseInterceptor, } from "http-proxy-middleware"; @@ -18,6 +19,7 @@ export function createContentProxy(): express.Handler { proxyTimeout: 20000, xfwd: true, selfHandleResponse: true, + onProxyReq: fixRequestBody, onProxyRes: responseInterceptor( async (responseBuffer, proxyRes, req, res) => { withProxyResponseHeaders(proxyRes, req, res); diff --git a/gcp/function/src/handlers/rumba.ts b/gcp/function/src/handlers/rumba.ts index 2eb42c1fbbf7..2b68ae098245 100644 --- a/gcp/function/src/handlers/rumba.ts +++ b/gcp/function/src/handlers/rumba.ts @@ -1,4 +1,4 @@ -import { createProxyMiddleware } from "http-proxy-middleware"; +import { createProxyMiddleware, fixRequestBody } from "http-proxy-middleware"; import { Source, sourceUri } from "../env.js"; @@ -8,4 +8,5 @@ export const proxyRumba = createProxyMiddleware({ autoRewrite: true, proxyTimeout: 20000, xfwd: true, + onProxyReq: fixRequestBody, }); From c0458790d10634a7f9f524591eace7d9096f3d44 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 00:18:22 +0200 Subject: [PATCH 228/343] refactor(gcp/function): rename withContentResponseHeaders --- gcp/function/src/handlers/content.ts | 4 ++-- gcp/function/src/headers.ts | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/gcp/function/src/handlers/content.ts b/gcp/function/src/handlers/content.ts index 2af5a7474b08..0d07e594d33c 100644 --- a/gcp/function/src/handlers/content.ts +++ b/gcp/function/src/handlers/content.ts @@ -5,7 +5,7 @@ import { responseInterceptor, } from "http-proxy-middleware"; -import { withProxyResponseHeaders } from "../headers.js"; +import { withContentResponseHeaders } from "../headers.js"; import { Source, sourceUri } from "../env.js"; const NOT_FOUND_PATH = "en-us/_spas/404.html"; @@ -22,7 +22,7 @@ export function createContentProxy(): express.Handler { onProxyReq: fixRequestBody, onProxyRes: responseInterceptor( async (responseBuffer, proxyRes, req, res) => { - withProxyResponseHeaders(proxyRes, req, res); + withContentResponseHeaders(proxyRes, req, res); if (proxyRes.statusCode === 404) { const response = await fetch(`${target}${NOT_FOUND_PATH}`); res.setHeader("Content-Type", "text/html"); diff --git a/gcp/function/src/headers.ts b/gcp/function/src/headers.ts index 29bba1cde6ef..a2101aed97c1 100644 --- a/gcp/function/src/headers.ts +++ b/gcp/function/src/headers.ts @@ -3,7 +3,7 @@ import type express from "express"; import { CSP_VALUE } from "./internal/constants/index.js"; -export function withProxyResponseHeaders( +export function withContentResponseHeaders( _proxyRes: IncomingMessage, req: IncomingMessage, res: ServerResponse @@ -14,7 +14,7 @@ export function withProxyResponseHeaders( const isLiveSampleURI = req.url?.includes("/_sample_.") ?? false; - setResponseHeaders((name, value) => res.setHeader(name, value), { + setContentResponseHeaders((name, value) => res.setHeader(name, value), { csp: !isLiveSampleURI && parseContentType(_proxyRes.headers["content-type"]).startsWith( @@ -41,11 +41,14 @@ export function withResponseHeaders( res: express.Response, options?: { csp?: boolean; xFrame?: boolean } ): express.Response { - setResponseHeaders((name, value) => res.set(name, value), options ?? {}); + setContentResponseHeaders( + (name, value) => res.set(name, value), + options ?? {} + ); return res; } -export function setResponseHeaders( +export function setContentResponseHeaders( setHeader: (name: string, value: string) => void, { csp = true, xFrame = true }: { csp?: boolean; xFrame?: boolean } ): void { From ac2da1dde3f3cca4b41f66253b521242caf12387 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 00:36:25 +0200 Subject: [PATCH 229/343] fix(gcp/function): set Cache-Control for content Hashed filenames are cached for 1 year, everything else for 1 day, except some files that don't get cached. --- gcp/function/src/headers.ts | 44 ++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/gcp/function/src/headers.ts b/gcp/function/src/headers.ts index a2101aed97c1..59c5592390be 100644 --- a/gcp/function/src/headers.ts +++ b/gcp/function/src/headers.ts @@ -3,8 +3,15 @@ import type express from "express"; import { CSP_VALUE } from "./internal/constants/index.js"; +const HASHED_MAX_AGE = 60 * 60 * 24 * 365; +const DEFAULT_MAX_AGE = 60 * 60 * 24; + +const NO_CACHE_VALUE = "no-store, must-revalidate"; + +const HASHED_REGEX = /\.[a-f0-9]{8,32}\./; + export function withContentResponseHeaders( - _proxyRes: IncomingMessage, + proxyRes: IncomingMessage, req: IncomingMessage, res: ServerResponse ): ServerResponse { @@ -12,12 +19,14 @@ export function withContentResponseHeaders( return res; } - const isLiveSampleURI = req.url?.includes("/_sample_.") ?? false; + const url = req.url ?? ""; + + const isLiveSampleURI = url.includes("/_sample_.") ?? false; setContentResponseHeaders((name, value) => res.setHeader(name, value), { csp: !isLiveSampleURI && - parseContentType(_proxyRes.headers["content-type"]).startsWith( + parseContentType(proxyRes.headers["content-type"]).startsWith( "text/html" ), xFrame: !isLiveSampleURI, @@ -28,9 +37,38 @@ export function withContentResponseHeaders( res.setHeader("Content-Encoding", "gzip"); } + const cacheControl = getCacheControl(proxyRes.statusCode ?? 0, url); + if (cacheControl) { + res.setHeader("Cache-Control", cacheControl); + } + return res; } +function getCacheControl(statusCode: number, url: string) { + if ( + statusCode === 404 || + url.endsWith("/service-worker.js") || + url.includes("/_whatsdeployed/") + ) { + return NO_CACHE_VALUE; + } + + if (200 <= statusCode && statusCode < 300) { + const maxAge = getCacheMaxAgeForUrl(url); + return `public, max-age=${maxAge}`; + } + + return null; +} + +function getCacheMaxAgeForUrl(url: string): number { + const isHashed = HASHED_REGEX.test(url); + const maxAge = isHashed ? HASHED_MAX_AGE : DEFAULT_MAX_AGE; + + return maxAge; +} + function parseContentType(value: unknown): string { const firstValue = Array.isArray(value) ? value[0] ?? "" : value; From 94ef0eaaea84c317b8d323285a5459cbccc756e8 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 00:54:30 +0200 Subject: [PATCH 230/343] fixup! chore(live-samples): use live-samples.mdn.mozilla/allizom.net (#8573) --- .github/workflows/stage-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index fec294dfe0dc..f60a0071525e 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -351,7 +351,7 @@ jobs: --memory=256MB \ --timeout=60s \ --set-env-vars="ORIGIN_MAIN=developer.allizom.org" \ - --set-env-vars="ORIGIN_LIVE_SAMPLES=yari-demos.developer.allizom.org" \ + --set-env-vars="ORIGIN_LIVE_SAMPLES=live-samples.mdn.allizom.net" \ --set-env-vars="SOURCE_CONTENT=https://storage.googleapis.com/content-stage-mdn/main/" \ --set-env-vars="SOURCE_RUMBA=https://api.developer.allizom.org/" \ --set-env-vars="SENTRY_DSN=${{ secrets.SENTRY_DSN_CLOUD_FUNCTION }}" \ From e255e1eb74ddabace3b09d0519f0f32246b2b761 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 12:19:11 +0200 Subject: [PATCH 231/343] fixup! refactor(gcp/function): merge routers + add HTTPS proxy --- gcp/function/package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/gcp/function/package.json b/gcp/function/package.json index 835aa18d0862..040a79e99736 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -18,6 +18,13 @@ "server:watch": "nodemon --exec npm run server", "start": "nf start" }, + "nodemonConfig": { + "ext": "json,ts", + "watch": [ + ".env", + "src" + ] + }, "dependencies": { "@adzerk/decision-sdk": "^1.0.0-beta.20", "@google-cloud/functions-framework": "^3.1.3", @@ -46,12 +53,5 @@ }, "engines": { "node": ">=18.0.0" - }, - "nodemonConfig": { - "ext": "json,ts", - "watch": [ - ".env", - "src" - ] } } From 4fef41a7eb5e1b102abe052217770ae849607762 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 13:07:30 +0200 Subject: [PATCH 232/343] fix(gcp/function): make middlewares + handlers async --- gcp/function/src/handlers/plans.ts | 2 +- gcp/function/src/middlewares/pathnameLC.ts | 2 +- gcp/function/src/middlewares/redirectFundamental.ts | 2 +- gcp/function/src/middlewares/redirectLeadingSlash.ts | 2 +- gcp/function/src/middlewares/redirectLocale.ts | 2 +- gcp/function/src/middlewares/redirectMovedPages.ts | 2 +- gcp/function/src/middlewares/redirectTrailingSlash.ts | 2 +- gcp/function/src/middlewares/resolveIndexHTML.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/gcp/function/src/handlers/plans.ts b/gcp/function/src/handlers/plans.ts index 7bfc417521a3..4d082e84af83 100644 --- a/gcp/function/src/handlers/plans.ts +++ b/gcp/function/src/handlers/plans.ts @@ -15,7 +15,7 @@ interface Result { plans: PlanResult; } -export function plans(req: express.Request, res: express.Response) { +export async function plans(req: express.Request, res: express.Response) { const lookupData = ORIGIN_MAIN === "developer.mozilla.org" ? prodLookup : stageLookup; diff --git a/gcp/function/src/middlewares/pathnameLC.ts b/gcp/function/src/middlewares/pathnameLC.ts index 9ac82839d4b7..fc325c7d727b 100644 --- a/gcp/function/src/middlewares/pathnameLC.ts +++ b/gcp/function/src/middlewares/pathnameLC.ts @@ -2,7 +2,7 @@ import * as url from "node:url"; import type express from "express"; -export function pathnameLC( +export async function pathnameLC( req: express.Request, _res: express.Response, next: express.NextFunction diff --git a/gcp/function/src/middlewares/redirectFundamental.ts b/gcp/function/src/middlewares/redirectFundamental.ts index 06c8e647a5f6..aac3f21de0cb 100644 --- a/gcp/function/src/middlewares/redirectFundamental.ts +++ b/gcp/function/src/middlewares/redirectFundamental.ts @@ -4,7 +4,7 @@ import { THIRTY_DAYS } from "../constants.js"; import { resolveFundamental } from "../internal/fundamental-redirects/index.js"; import { redirect } from "../utils.js"; -export function redirectFundamental( +export async function redirectFundamental( req: express.Request, res: express.Response, next: express.NextFunction diff --git a/gcp/function/src/middlewares/redirectLeadingSlash.ts b/gcp/function/src/middlewares/redirectLeadingSlash.ts index 6f9d30aa81da..64e41bd846e6 100644 --- a/gcp/function/src/middlewares/redirectLeadingSlash.ts +++ b/gcp/function/src/middlewares/redirectLeadingSlash.ts @@ -13,7 +13,7 @@ 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 function redirectLeadingSlash( +export async function redirectLeadingSlash( req: express.Request, res: express.Response, next: express.NextFunction diff --git a/gcp/function/src/middlewares/redirectLocale.ts b/gcp/function/src/middlewares/redirectLocale.ts index 0d02a559c09c..dec70253fc6f 100644 --- a/gcp/function/src/middlewares/redirectLocale.ts +++ b/gcp/function/src/middlewares/redirectLocale.ts @@ -6,7 +6,7 @@ import { redirect } from "../utils.js"; const NEEDS_LOCALE = /^\/(?:docs|search|settings|signin|signup|plus)(?:$|\/)/; -export function redirectLocale( +export async function redirectLocale( req: express.Request, res: express.Response, next: express.NextFunction diff --git a/gcp/function/src/middlewares/redirectMovedPages.ts b/gcp/function/src/middlewares/redirectMovedPages.ts index 5aca37df6e48..fed2aa78d693 100644 --- a/gcp/function/src/middlewares/redirectMovedPages.ts +++ b/gcp/function/src/middlewares/redirectMovedPages.ts @@ -10,7 +10,7 @@ const require = createRequire(import.meta.url); const REDIRECTS = require("../../redirects.json"); const REDIRECT_SUFFIXES = ["/index.json", "/bcd.json", ""]; -export function redirectMovedPages( +export async function redirectMovedPages( req: express.Request, res: express.Response, next: express.NextFunction diff --git a/gcp/function/src/middlewares/redirectTrailingSlash.ts b/gcp/function/src/middlewares/redirectTrailingSlash.ts index 113b45f075ee..0369971941d3 100644 --- a/gcp/function/src/middlewares/redirectTrailingSlash.ts +++ b/gcp/function/src/middlewares/redirectTrailingSlash.ts @@ -25,7 +25,7 @@ const LEGACY_URI_NEEDING_TRAILING_SLASH = new RegExp( )})?/(?:account|contribute|maintenance-mode|payments)/?$` ); -export function redirectTrailingSlash( +export async function redirectTrailingSlash( req: express.Request, res: express.Response, next: express.NextFunction diff --git a/gcp/function/src/middlewares/resolveIndexHTML.ts b/gcp/function/src/middlewares/resolveIndexHTML.ts index 85cd526d6ba3..acb5933d3c50 100644 --- a/gcp/function/src/middlewares/resolveIndexHTML.ts +++ b/gcp/function/src/middlewares/resolveIndexHTML.ts @@ -2,7 +2,7 @@ import type express from "express"; import { slugToFolder } from "../internal/slug-utils/index.js"; import * as path from "node:path"; -export function resolveIndexHTML( +export async function resolveIndexHTML( req: express.Request, _res: express.Response, next: express.NextFunction From 9acb8fa9409d21d43a569f0098c45f1318814950 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 13:25:27 +0200 Subject: [PATCH 233/343] chore(gcp/function): set --concurrency=8 --- .github/workflows/stage-build.yml | 1 + .github/workflows/xyz-build.yml | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index f60a0071525e..f7b1c942af34 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -348,6 +348,7 @@ jobs: --trigger-http \ --allow-unauthenticated \ --entry-point=mdnHandler \ + --concurrency=8 \ --memory=256MB \ --timeout=60s \ --set-env-vars="ORIGIN_MAIN=developer.allizom.org" \ diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 100bc86ae8e4..9dc06d380fa7 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -198,8 +198,7 @@ jobs: --trigger-http \ --allow-unauthenticated \ --entry-point=mdnHandler \ - --min-instances=1 \ - --max-instances=1000 \ + --concurrency=8 \ --memory=256MB \ --timeout=30s \ --set-env-vars="ORIGIN_MAIN=${{ vars.ORIGIN_MAIN }}" \ From 15f1e7d4371328cf8f25905129de25d5ce3fe612 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 13:26:41 +0200 Subject: [PATCH 234/343] ci(stage-build): run on push to gcp --- .github/workflows/stage-build.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index f7b1c942af34..c7db578a705d 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -18,6 +18,12 @@ on: schedule: # * is a special character in YAML so you have to quote this string - cron: "0 */24 * * *" + push: + branches: + - gcp + paths: + - .github/workflows/stage-build.yml + - gcp/function/** workflow_dispatch: inputs: From a1e72e084edc6592d386138114954c8c89741986 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 13:32:07 +0200 Subject: [PATCH 235/343] fix(gcp/function): use gcloud beta functions Required to set `--concurrency`. --- .github/workflows/stage-build.yml | 2 +- .github/workflows/xyz-build.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index c7db578a705d..3c9b33fdc517 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -346,7 +346,7 @@ jobs: if: ${{ ! vars.SKIP_FUNCTION }} run: |- for region in europe-west1 us-west1 asia-east1; do - gcloud functions deploy mdn-nonprod-stage-$region \ + gcloud beta functions deploy mdn-nonprod-stage-$region \ --gen2 \ --runtime=nodejs18 \ --region=$region \ diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 9dc06d380fa7..cff19ed2e59f 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -190,7 +190,7 @@ jobs: if: ${{ ! vars.SKIP_FUNCTION }} run: |- for region in europe-west1 us-west1 asia-east1; do - gcloud functions deploy mdn-xyz-$region \ + gcloud beta functions deploy mdn-xyz-$region \ --gen2 \ --runtime=nodejs18 \ --region=$region \ From 71d2bf57392345b51b25fabd365c2616f893df7f Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 13:44:27 +0200 Subject: [PATCH 236/343] fix(gcp/function): call gcloud beta with --quiet --- .github/workflows/stage-build.yml | 1 + .github/workflows/xyz-build.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 3c9b33fdc517..a1d1d6e8df9e 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -347,6 +347,7 @@ jobs: run: |- for region in europe-west1 us-west1 asia-east1; do gcloud beta functions deploy mdn-nonprod-stage-$region \ + --quiet \ --gen2 \ --runtime=nodejs18 \ --region=$region \ diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index cff19ed2e59f..f2baa4a10d36 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -191,6 +191,7 @@ jobs: run: |- for region in europe-west1 us-west1 asia-east1; do gcloud beta functions deploy mdn-xyz-$region \ + --quiet \ --gen2 \ --runtime=nodejs18 \ --region=$region \ From b088f73fe490c4de17c2f0383b567d897ee260d3 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 13:46:33 +0200 Subject: [PATCH 237/343] ci(stage-build): use setup-gcloud with install_components instead of --quiet --- .github/workflows/stage-build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index a1d1d6e8df9e..0c3da0e955e3 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -332,6 +332,8 @@ jobs: - name: Setup gcloud if: ${{ ! vars.SKIP_FUNCTION || !vars.SKIP_INVALIDATE }} uses: google-github-actions/setup-gcloud@v1 + with: + install_components: 'beta' - name: Generate redirects map if: ${{ ! vars.SKIP_FUNCTION }} @@ -347,7 +349,6 @@ jobs: run: |- for region in europe-west1 us-west1 asia-east1; do gcloud beta functions deploy mdn-nonprod-stage-$region \ - --quiet \ --gen2 \ --runtime=nodejs18 \ --region=$region \ From 5e7e76a06aa52e34614a82516d4d5b0d8e170b09 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 13:52:32 +0200 Subject: [PATCH 238/343] ci(stage-build): set --cpu=1 to allow concurrency Resolves the following error: ``` spec.template.spec.containers.resources.limits.cpu: Invalid value specified for cpu. Total cpu < 1 is not supported with concurrency > 1. ``` --- .github/workflows/stage-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 0c3da0e955e3..cb506898bd88 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -356,6 +356,7 @@ jobs: --trigger-http \ --allow-unauthenticated \ --entry-point=mdnHandler \ + --cpu=1 \ --concurrency=8 \ --memory=256MB \ --timeout=60s \ From 411577e67663ba7c53ae6bdf05c43e04b0d50879 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 14:04:34 +0200 Subject: [PATCH 239/343] ci(stage-build): set --memory=2GB This automatically adjusts the CPU value. --- .github/workflows/stage-build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index cb506898bd88..531d127a422a 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -356,9 +356,8 @@ jobs: --trigger-http \ --allow-unauthenticated \ --entry-point=mdnHandler \ - --cpu=1 \ --concurrency=8 \ - --memory=256MB \ + --memory=2GB \ --timeout=60s \ --set-env-vars="ORIGIN_MAIN=developer.allizom.org" \ --set-env-vars="ORIGIN_LIVE_SAMPLES=live-samples.mdn.allizom.net" \ From a7575b6f737819f4f3be6d4e7261201de8b61ba9 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 14:07:34 +0200 Subject: [PATCH 240/343] ci(stage-build): avoid yarn install if we don't build --- .github/workflows/stage-build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 531d127a422a..4a5ba9462762 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -115,7 +115,7 @@ jobs: cache: yarn - name: Install all yarn packages - if: ${{ ! vars.SKIP_BUILD || ! vars.SKIP_FUNCTION }} + if: ${{ ! vars.SKIP_BUILD }} run: yarn --frozen-lockfile - name: Install Python @@ -342,6 +342,7 @@ jobs: CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files run: | + npm ci npm run redirects - name: Deploy Function From 864b9858728e9eac71f2239b3939cf2240f1219e Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 14:55:46 +0200 Subject: [PATCH 241/343] chore(gcp/function): add 404 fallback This should prevent timeouts e.g. when POSTing to routes that don't support POST. --- gcp/function/src/app.ts | 2 ++ gcp/function/src/middlewares/notFound.ts | 5 +++++ 2 files changed, 7 insertions(+) create mode 100644 gcp/function/src/middlewares/notFound.ts diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 19598b6d9f66..833902fda942 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -16,6 +16,7 @@ import { redirectFundamental } from "./middlewares/redirectFundamental.js"; import { redirectLocale } from "./middlewares/redirectLocale.js"; import { redirectTrailingSlash } from "./middlewares/redirectTrailingSlash.js"; import { requireOrigin } from "./middlewares/requireOrigin.js"; +import { notFound } from "./middlewares/notFound.js"; const proxyContent = createContentProxy(); @@ -72,6 +73,7 @@ router.get( resolveIndexHTML, proxyContent ); +router.all("*", notFound); export function createHandler() { return async (req: Request, res: Response) => diff --git a/gcp/function/src/middlewares/notFound.ts b/gcp/function/src/middlewares/notFound.ts new file mode 100644 index 000000000000..4c32ac8a0ae0 --- /dev/null +++ b/gcp/function/src/middlewares/notFound.ts @@ -0,0 +1,5 @@ +import type { Request, Response } from "express"; + +export async function notFound(_req: Request, res: Response) { + res.send(404); +} From e7eb8566b5c04f31067a5d952c7d0c9f39d80fe8 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 15:06:43 +0200 Subject: [PATCH 242/343] ci(stage-build): increase max-instances from 100 to 1000 --- .github/workflows/stage-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 4a5ba9462762..881b4cc5f12c 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -358,6 +358,7 @@ jobs: --allow-unauthenticated \ --entry-point=mdnHandler \ --concurrency=8 \ + --max-instances=1000 \ --memory=2GB \ --timeout=60s \ --set-env-vars="ORIGIN_MAIN=developer.allizom.org" \ From fe88c2f8f6399d3fe66d2ab54c2428b28e8a6a88 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 15:20:05 +0200 Subject: [PATCH 243/343] ci(stage-build): set --min-instances=100 --- .github/workflows/stage-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 881b4cc5f12c..75d79270f325 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -358,6 +358,7 @@ jobs: --allow-unauthenticated \ --entry-point=mdnHandler \ --concurrency=8 \ + --min-instances=100 \ --max-instances=1000 \ --memory=2GB \ --timeout=60s \ From e7afa1c9b6405fc1d5124fee4563bdf98edad48f Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 15:26:56 +0200 Subject: [PATCH 244/343] ci(stage-build): reduce timeout to 30s --- .github/workflows/stage-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 75d79270f325..f72d2c1d1a00 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -361,7 +361,7 @@ jobs: --min-instances=100 \ --max-instances=1000 \ --memory=2GB \ - --timeout=60s \ + --timeout=30s \ --set-env-vars="ORIGIN_MAIN=developer.allizom.org" \ --set-env-vars="ORIGIN_LIVE_SAMPLES=live-samples.mdn.allizom.net" \ --set-env-vars="SOURCE_CONTENT=https://storage.googleapis.com/content-stage-mdn/main/" \ From 0c3097a31df4c2f17c85d223fffac627d873987c Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 15:34:58 +0200 Subject: [PATCH 245/343] ci(stage-build): increase concurrency from 8 to 16 --- .github/workflows/stage-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index f72d2c1d1a00..c2c16b59c742 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -357,7 +357,7 @@ jobs: --trigger-http \ --allow-unauthenticated \ --entry-point=mdnHandler \ - --concurrency=8 \ + --concurrency=16 \ --min-instances=100 \ --max-instances=1000 \ --memory=2GB \ From 66b8f74b29b0c7dc3afcf101975dc97decfea56f Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 15:47:55 +0200 Subject: [PATCH 246/343] feat(gcp/function): add shortcut route for / --- gcp/function/src/app.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 833902fda942..920ececb1b63 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -37,6 +37,7 @@ router.all("/pong/*", requireOrigin(Origin.main), express.json(), proxyKevel); router.all("/pimg/*", requireOrigin(Origin.main), proxyKevel); router.get("/sitemaps/*", requireOrigin(Origin.main), proxyContent); router.get("/static/*", requireOrigin(Origin.main), proxyContent); +router.get("/", requireOrigin(Origin.main), redirectLocale); router.get( "/[^/]+/docs/*/_sample_.*.html", requireOrigin(Origin.liveSamples), From 27ef2a46c0c43641e20dcb0c58bc762d30194d31 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 17:50:26 +0200 Subject: [PATCH 247/343] ci(xyz-build): test with 1 instance @ 16G / 4-cores --- .github/workflows/xyz-build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index f2baa4a10d36..2888f1ee20ef 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -199,8 +199,10 @@ jobs: --trigger-http \ --allow-unauthenticated \ --entry-point=mdnHandler \ + --min-instances=1 \ + --max-instances=1 \ --concurrency=8 \ - --memory=256MB \ + --memory=16G \ --timeout=30s \ --set-env-vars="ORIGIN_MAIN=${{ vars.ORIGIN_MAIN }}" \ --set-env-vars="ORIGIN_LIVE_SAMPLES=${{ vars.ORIGIN_LIVE_SAMPLES }}" \ From a0f4f160127490ee37ad7ec8349e20bc8fc3772a Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 17:51:14 +0200 Subject: [PATCH 248/343] ci(xyz-build): avoid --quiet --- .github/workflows/xyz-build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 2888f1ee20ef..b12d83bb3e19 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -170,6 +170,8 @@ jobs: - name: Setup gcloud uses: google-github-actions/setup-gcloud@v1 + with: + install_components: 'beta' - name: Sync build with GCS bucket if: ${{ ! vars.SKIP_BUILD }} @@ -191,7 +193,6 @@ jobs: run: |- for region in europe-west1 us-west1 asia-east1; do gcloud beta functions deploy mdn-xyz-$region \ - --quiet \ --gen2 \ --runtime=nodejs18 \ --region=$region \ From 52304ea7a7e9cafd8038e2a2ee26a33f8955145d Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 17:59:57 +0200 Subject: [PATCH 249/343] ci(xyz-build): avoid yarn install if SKIP_BUILD --- .github/workflows/xyz-build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index b12d83bb3e19..fa4e4ad2b45b 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -65,6 +65,7 @@ jobs: cache: yarn - name: Install all yarn packages + if: ${{ ! vars.SKIP_BUILD }} run: yarn --frozen-lockfile - name: Print information about CPU @@ -186,6 +187,7 @@ jobs: CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files run: | + npm ci npm run redirects - name: Deploy Function From 933a7a863bde6bb2994849a93ee6229b3d75f51d Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 18:12:44 +0200 Subject: [PATCH 250/343] ci(xyz-build): increase concurrency from 8 to 64 --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index fa4e4ad2b45b..d32df213409b 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -204,7 +204,7 @@ jobs: --entry-point=mdnHandler \ --min-instances=1 \ --max-instances=1 \ - --concurrency=8 \ + --concurrency=32 \ --memory=16G \ --timeout=30s \ --set-env-vars="ORIGIN_MAIN=${{ vars.ORIGIN_MAIN }}" \ From 50a98ee59c3035c158dbab6d8ee1663cefbd8a0e Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 18:32:01 +0200 Subject: [PATCH 251/343] fix(gcp/function): lowercase image pathnames in /docs --- gcp/function/src/app.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/gcp/function/src/app.ts b/gcp/function/src/app.ts index 920ececb1b63..934b0e103737 100644 --- a/gcp/function/src/app.ts +++ b/gcp/function/src/app.ts @@ -47,6 +47,7 @@ router.get( router.get( "/[^/]+/docs/*/*.(png|jpeg|jpg|gif|svg|webp)", requireOrigin(Origin.main, Origin.liveSamples), + pathnameLC, proxyContent ); router.get( From 796d59c08a3b833f2058b41783b983f6a6ca97bb Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 19:23:59 +0200 Subject: [PATCH 252/343] ci(workflows): lint --- .github/workflows/stage-build.yml | 2 +- .github/workflows/xyz-build.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index c2c16b59c742..cc2f1fdd40af 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -333,7 +333,7 @@ jobs: if: ${{ ! vars.SKIP_FUNCTION || !vars.SKIP_INVALIDATE }} uses: google-github-actions/setup-gcloud@v1 with: - install_components: 'beta' + install_components: "beta" - name: Generate redirects map if: ${{ ! vars.SKIP_FUNCTION }} diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index d32df213409b..4ad37cc307f8 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -172,7 +172,7 @@ jobs: - name: Setup gcloud uses: google-github-actions/setup-gcloud@v1 with: - install_components: 'beta' + install_components: "beta" - name: Sync build with GCS bucket if: ${{ ! vars.SKIP_BUILD }} From 834e562586e9caef3d9302458ce0123908b39f28 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 22:32:07 +0200 Subject: [PATCH 253/343] ci(xyz-build): increase concurrency from 32 to 100 --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 4ad37cc307f8..c442ee441839 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -204,7 +204,7 @@ jobs: --entry-point=mdnHandler \ --min-instances=1 \ --max-instances=1 \ - --concurrency=32 \ + --concurrency=100 \ --memory=16G \ --timeout=30s \ --set-env-vars="ORIGIN_MAIN=${{ vars.ORIGIN_MAIN }}" \ From 85000b7b4a36cc249b881f420b81f49f67e5fb50 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 22:35:03 +0200 Subject: [PATCH 254/343] ci(stage-build): update scaling - 1 to 100 instances. - 16 GB, i.e. 4 vCPUs. - 100 concurrent requests per instance. --- .github/workflows/stage-build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index cc2f1fdd40af..f2ca723ee8ef 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -357,10 +357,10 @@ jobs: --trigger-http \ --allow-unauthenticated \ --entry-point=mdnHandler \ - --concurrency=16 \ - --min-instances=100 \ - --max-instances=1000 \ - --memory=2GB \ + --concurrency=100 \ + --min-instances=1 \ + --max-instances=100 \ + --memory=16GB \ --timeout=30s \ --set-env-vars="ORIGIN_MAIN=developer.allizom.org" \ --set-env-vars="ORIGIN_LIVE_SAMPLES=live-samples.mdn.allizom.net" \ From 87d5b1640dc6c64a97e33f40ec0ce3f1dc107ab3 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 12 Apr 2023 22:47:02 +0200 Subject: [PATCH 255/343] ci(xyz-build): increase max-instances from 1 to 100 --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index c442ee441839..fa15afb3f09d 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -203,7 +203,7 @@ jobs: --allow-unauthenticated \ --entry-point=mdnHandler \ --min-instances=1 \ - --max-instances=1 \ + --max-instances=100 \ --concurrency=100 \ --memory=16G \ --timeout=30s \ From 4bca8a5a9de0bad4358680aa6eda83e5fbfed46a Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 13 Apr 2023 14:44:48 +0200 Subject: [PATCH 256/343] chore(gcp): use 2GB function Node.js is single-threaded, so more cores have no effect. --- .github/workflows/stage-build.yml | 2 +- .github/workflows/xyz-build.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index f2ca723ee8ef..2e1a6d151be9 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -360,7 +360,7 @@ jobs: --concurrency=100 \ --min-instances=1 \ --max-instances=100 \ - --memory=16GB \ + --memory=2GB \ --timeout=30s \ --set-env-vars="ORIGIN_MAIN=developer.allizom.org" \ --set-env-vars="ORIGIN_LIVE_SAMPLES=live-samples.mdn.allizom.net" \ diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index fa15afb3f09d..cd0ea638c7cc 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -205,7 +205,7 @@ jobs: --min-instances=1 \ --max-instances=100 \ --concurrency=100 \ - --memory=16G \ + --memory=2GB \ --timeout=30s \ --set-env-vars="ORIGIN_MAIN=${{ vars.ORIGIN_MAIN }}" \ --set-env-vars="ORIGIN_LIVE_SAMPLES=${{ vars.ORIGIN_LIVE_SAMPLES }}" \ From 6c956685a4c47b04300ffede2da61d1ec1e8e38c Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 13 Apr 2023 14:45:31 +0200 Subject: [PATCH 257/343] ci(stage-build): set SENTRY_TRACES_SAMPLE_RATE Even if it is not set. --- .github/workflows/stage-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 2e1a6d151be9..7ea96c541a25 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -368,6 +368,7 @@ jobs: --set-env-vars="SOURCE_RUMBA=https://api.developer.allizom.org/" \ --set-env-vars="SENTRY_DSN=${{ secrets.SENTRY_DSN_CLOUD_FUNCTION }}" \ --set-env-vars="SENTRY_ENVIRONMENT=stage" \ + --set-env-vars="SENTRY_TRACES_SAMPLE_RATE=${{ vars.SENTRY_TRACES_SAMPLE_RATE }}" \ --set-env-vars="SENTRY_RELEASE=${{ github.sha }}" \ --set-secrets="KEVEL_SITE_ID=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-kevel-site-id/versions/latest" \ --set-secrets="KEVEL_NETWORK_ID=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/stage-kevel-network-id/versions/latest" \ From b527f7db072d9f1c6197692baaabaca09edc32d5 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 13 Apr 2023 14:51:04 +0200 Subject: [PATCH 258/343] ci(prod-build): replicate GCP changes from stage-build --- .github/workflows/prod-build.yml | 110 +++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/.github/workflows/prod-build.yml b/.github/workflows/prod-build.yml index 6a12804f5a3b..44c5e5bb6271 100644 --- a/.github/workflows/prod-build.yml +++ b/.github/workflows/prod-build.yml @@ -42,8 +42,20 @@ on: required: false default: ${DEFAULT_LOG_EACH_SUCCESSFUL_UPLOAD} + workflow_call: + secrets: + GCP_PROJECT_NAME: + required: true + WIP_PROJECT_ID: + required: true + jobs: build: + environment: prod + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest # Only run the scheduled workflows on the main repo. @@ -53,6 +65,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/checkout@v3 + if: ${{ ! vars.SKIP_BUILD || ! vars.SKIP_FUNCTION }} with: repository: mdn/content path: mdn/content @@ -75,6 +88,7 @@ jobs: echo "DEPLOYER_LOG_EACH_SUCCESSFUL_UPLOAD=${{ github.event.inputs.log_each_successful_upload || env.DEFAULT_LOG_EACH_SUCCESSFUL_UPLOAD }}" >> $GITHUB_ENV - uses: actions/checkout@v3 + if: ${{ ! vars.SKIP_BUILD || ! vars.SKIP_FUNCTION }} with: repository: mdn/translated-content path: mdn/translated-content @@ -82,33 +96,40 @@ jobs: fetch-depth: 0 - uses: actions/checkout@v3 + if: ${{ ! vars.SKIP_BUILD }} with: repository: mdn/mdn-contributor-spotlight path: mdn/mdn-contributor-spotlight - name: Setup Node.js environment + if: ${{ ! vars.SKIP_BUILD || ! vars.SKIP_FUNCTION }} uses: actions/setup-node@v3 with: node-version: 18 cache: yarn - name: Install all yarn packages + if: ${{ ! vars.SKIP_BUILD }} run: yarn --frozen-lockfile - name: Install Python + if: ${{ ! vars.SKIP_BUILD }} uses: actions/setup-python@v4 with: python-version: "3.10" - name: Install Python poetry + if: ${{ ! vars.SKIP_BUILD }} uses: snok/install-poetry@v1 - name: Install deployer + if: ${{ ! vars.SKIP_BUILD }} run: | cd deployer poetry install - name: Display Python & Poetry version + if: ${{ ! vars.SKIP_BUILD }} run: | python --version poetry --version @@ -123,6 +144,7 @@ jobs: run: cat /proc/cpuinfo - name: Build everything + if: ${{ ! vars.SKIP_BUILD }} env: # Remember, the mdn/content repo got cloned into `pwd` into a # sub-folder called "mdn/content" @@ -223,6 +245,7 @@ jobs: yarn build --sitemap-index - name: Deploy with deployer + if: ${{ ! (vars.SKIP_BUILD || vars.SKIP_AWS) }} env: GITHUB_SHA: ${{ env.GITHUB_SHA }} GITHUB_RUN_ID: ${{ env.GITHUB_RUN_ID }} @@ -263,6 +286,7 @@ jobs: poetry run deployer search-index ../client/build - name: Configure AWS Credentials + if: ${{ ! (vars.SKIP_INVALIDATE || vars.SKIP_AWS) }} uses: aws-actions/configure-aws-credentials@v1-node16 with: aws-access-key-id: ${{ secrets.DEPLOYER_PROD_AWS_ACCESS_KEY_ID }} @@ -270,11 +294,97 @@ jobs: aws-region: us-east-1 - name: Invalidate CDN + if: ${{ ! (vars.SKIP_INVALIDATE || vars.SKIP_AWS) }} env: DISTRIBUTION: E2ZY2DGUN70EMI PATHS: /* run: aws cloudfront create-invalidation --distribution-id "$DISTRIBUTION" --paths "$PATHS" + - name: Authenticate with GCP + if: ${{ ! vars.SKIP_BUILD }} + uses: google-github-actions/auth@v1 + with: + token_format: access_token + service_account: deploy-prod-content@${{ secrets.GCP_PROJECT_NAME }}.iam.gserviceaccount.com + workload_identity_provider: projects/${{ secrets.WIP_PROJECT_ID }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions + + - name: Setup gcloud + if: ${{ ! vars.SKIP_BUILD }} + uses: google-github-actions/setup-gcloud@v1 + + - name: Sync Yari Content + if: ${{ ! vars.SKIP_BUILD }} + run: |- + gsutil -m -h "Cache-Control:public, max-age=86400" rsync -crj html,json,txt client/build gs://content-prod-mdn/main + + # Deploy Function + - name: Authenticate with GCP + if: ${{ ! vars.SKIP_FUNCTION || !vars.SKIP_INVALIDATE }} + uses: google-github-actions/auth@v1 + with: + token_format: access_token + service_account: deploy-prod-mdn-ingre@${{ secrets.GCP_PROJECT_NAME }}.iam.gserviceaccount.com + workload_identity_provider: projects/${{ secrets.WIP_PROJECT_ID }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions + + - name: Setup gcloud + if: ${{ ! vars.SKIP_FUNCTION || !vars.SKIP_INVALIDATE }} + uses: google-github-actions/setup-gcloud@v1 + with: + install_components: "beta" + + - name: Generate redirects map + if: ${{ ! vars.SKIP_FUNCTION }} + working-directory: gcp/function + env: + CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files + CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files + run: | + npm ci + npm run redirects + + - name: Deploy Function + if: ${{ ! vars.SKIP_FUNCTION }} + run: |- + for region in europe-west1 us-west1 asia-east1; do + gcloud beta functions deploy mdn-prod-$region \ + --gen2 \ + --runtime=nodejs18 \ + --region=$region \ + --source=gcp/function \ + --trigger-http \ + --allow-unauthenticated \ + --entry-point=mdnHandler \ + --concurrency=100 \ + --min-instances=10 \ + --max-instances=1000 \ + --memory=2GB \ + --timeout=30s \ + --set-env-vars="ORIGIN_MAIN=developer.mozilla.org" \ + --set-env-vars="ORIGIN_LIVE_SAMPLES=live-samples.mdn.mozilla.net" \ + --set-env-vars="SOURCE_CONTENT=https://storage.googleapis.com/content-prod-mdn/main/" \ + --set-env-vars="SOURCE_RUMBA=https://api.developer.mozilla.org/" \ + --set-env-vars="SENTRY_DSN=${{ secrets.SENTRY_DSN_CLOUD_FUNCTION }}" \ + --set-env-vars="SENTRY_ENVIRONMENT=prod" \ + --set-env-vars="SENTRY_TRACES_SAMPLE_RATE=${{ vars.SENTRY_TRACES_SAMPLE_RATE }}" \ + --set-env-vars="SENTRY_RELEASE=${{ github.sha }}" \ + --set-secrets="KEVEL_SITE_ID=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/prod-kevel-site-id/versions/latest" \ + --set-secrets="KEVEL_NETWORK_ID=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/prod-kevel-network-id/versions/latest" \ + --set-secrets="SIGN_SECRET=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/prod-sign-secret/versions/latest" \ + --set-secrets="CARBON_ZONE_KEY=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/prod-carbon-zone-key/versions/latest" \ + --set-secrets="CARBON_FALLBACK_ENABLED=projects/${{ secrets.GCP_PROJECT_NAME }}/secrets/prod-fallback-enabled/versions/latest" \ + 2>&1 | sed "s/^/[$region] /" & + pids+=($!) + done + + for pid in "${pids[@]}"; do + wait $pid + done + + - name: Invalidate CDN + if: ${{ ! vars.SKIP_INVALIDATE }} + run: |- + gcloud compute url-maps invalidate-cdn-cache ${{ secrets.GCP_LOAD_BALANCER_NAME }} --path "/*" + - name: Slack Notification if: failure() uses: rtCamp/action-slack-notify@v2 From 290550d773ce8ad5ca8e98d3192e1ea909fdf148 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 13 Apr 2023 16:49:13 +0200 Subject: [PATCH 259/343] fixup! ci(prod-build): replicate GCP changes from stage-build --- .github/workflows/prod-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prod-build.yml b/.github/workflows/prod-build.yml index 44c5e5bb6271..0920965da906 100644 --- a/.github/workflows/prod-build.yml +++ b/.github/workflows/prod-build.yml @@ -300,7 +300,7 @@ jobs: PATHS: /* run: aws cloudfront create-invalidation --distribution-id "$DISTRIBUTION" --paths "$PATHS" - - name: Authenticate with GCP + - name: Authenticate with GCP if: ${{ ! vars.SKIP_BUILD }} uses: google-github-actions/auth@v1 with: From 41c195312faa429da29ef453d329091ba4368073 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 13 Apr 2023 17:00:32 +0200 Subject: [PATCH 260/343] fixup! ci(prod-build): replicate GCP changes from stage-build --- .github/workflows/prod-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prod-build.yml b/.github/workflows/prod-build.yml index 0920965da906..76cb16cd5cee 100644 --- a/.github/workflows/prod-build.yml +++ b/.github/workflows/prod-build.yml @@ -323,7 +323,7 @@ jobs: uses: google-github-actions/auth@v1 with: token_format: access_token - service_account: deploy-prod-mdn-ingre@${{ secrets.GCP_PROJECT_NAME }}.iam.gserviceaccount.com + service_account: deploy-prod-prod-mdn-ingress@${{ secrets.GCP_PROJECT_NAME }}.iam.gserviceaccount.com workload_identity_provider: projects/${{ secrets.WIP_PROJECT_ID }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions - name: Setup gcloud From ea7ddb5be2ce7bc299ed7a7ce3baf3f24b643607 Mon Sep 17 00:00:00 2001 From: Claas Augner <495429+caugner@users.noreply.github.com> Date: Thu, 13 Apr 2023 17:05:57 +0200 Subject: [PATCH 261/343] Update .github/workflows/prod-build.yml Co-authored-by: Brett Kochendorfer --- .github/workflows/prod-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prod-build.yml b/.github/workflows/prod-build.yml index 76cb16cd5cee..19941a0cec76 100644 --- a/.github/workflows/prod-build.yml +++ b/.github/workflows/prod-build.yml @@ -346,7 +346,7 @@ jobs: if: ${{ ! vars.SKIP_FUNCTION }} run: |- for region in europe-west1 us-west1 asia-east1; do - gcloud beta functions deploy mdn-prod-$region \ + gcloud beta functions deploy mdn-prod-prod-$region \ --gen2 \ --runtime=nodejs18 \ --region=$region \ From 0df5c1c308622da6fedb56bc9c8367da7b49ab22 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 13 Apr 2023 17:22:31 +0200 Subject: [PATCH 262/343] fix(gcp/function): use Cloudfront-Viewer-Country header --- gcp/function/src/utils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gcp/function/src/utils.ts b/gcp/function/src/utils.ts index 6429b2186d07..0cdade49240e 100644 --- a/gcp/function/src/utils.ts +++ b/gcp/function/src/utils.ts @@ -2,8 +2,7 @@ import type express from "express"; import { DEFAULT_COUNTRY } from "./constants.js"; export function getRequestCountry(req: express.Request): string { - // https://cloud.google.com/appengine/docs/flexible/reference/request-headers#app_engine-specific_headers - const value = req.headers["x-appengine-country"]; + const value = req.headers["cloudfront-viewer-country"]; if (typeof value === "string" && value !== "ZZ") { return value; From bd0bf45e546f4aaa523ee94701e8183939d22bf7 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 13 Apr 2023 17:59:35 +0200 Subject: [PATCH 263/343] chore(gcp/function): warn if headers already sent --- gcp/function/src/headers.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gcp/function/src/headers.ts b/gcp/function/src/headers.ts index 59c5592390be..cd6f48aba98f 100644 --- a/gcp/function/src/headers.ts +++ b/gcp/function/src/headers.ts @@ -16,6 +16,9 @@ export function withContentResponseHeaders( res: ServerResponse ): ServerResponse { if (res.headersSent) { + console.warn( + `Cannot set content response headers. Headers already sent for: ${req.url}` + ); return res; } From 7f142ab231983306e58c21cafee2c4f8ffa8cd90 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 13 Apr 2023 21:18:24 +0200 Subject: [PATCH 264/343] chore(gcp): copy static first, rsync -d, introduce GCP_BUCKET_NAME var --- .github/workflows/prod-build.yml | 7 ++++--- .github/workflows/stage-build.yml | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/prod-build.yml b/.github/workflows/prod-build.yml index 19941a0cec76..6c8ce0f40f42 100644 --- a/.github/workflows/prod-build.yml +++ b/.github/workflows/prod-build.yml @@ -315,7 +315,8 @@ jobs: - name: Sync Yari Content if: ${{ ! vars.SKIP_BUILD }} run: |- - gsutil -m -h "Cache-Control:public, max-age=86400" rsync -crj html,json,txt client/build gs://content-prod-mdn/main + gsutil -q -m -h "Cache-Control: public, max-age=86400" cp -r client/build/static gs://${{ vars.GCP_BUCKET_NAME }}/main/static + gsutil -q -m -h "Cache-Control: public, max-age=86400" rsync -cdrj html,json,txt client/build gs://${{ vars.GCP_BUCKET_NAME }}/main # Deploy Function - name: Authenticate with GCP @@ -338,7 +339,7 @@ jobs: env: CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files - run: | + run: |- npm ci npm run redirects @@ -361,7 +362,7 @@ jobs: --timeout=30s \ --set-env-vars="ORIGIN_MAIN=developer.mozilla.org" \ --set-env-vars="ORIGIN_LIVE_SAMPLES=live-samples.mdn.mozilla.net" \ - --set-env-vars="SOURCE_CONTENT=https://storage.googleapis.com/content-prod-mdn/main/" \ + --set-env-vars="SOURCE_CONTENT=https://storage.googleapis.com/${{ vars.GCP_BUCKET_NAME }}/main/" \ --set-env-vars="SOURCE_RUMBA=https://api.developer.mozilla.org/" \ --set-env-vars="SENTRY_DSN=${{ secrets.SENTRY_DSN_CLOUD_FUNCTION }}" \ --set-env-vars="SENTRY_ENVIRONMENT=prod" \ diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 7ea96c541a25..527d0bf68a99 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -318,7 +318,8 @@ jobs: - name: Sync Yari Content if: ${{ ! vars.SKIP_BUILD }} run: |- - gsutil -m -h "Cache-Control:public, max-age=86400" rsync -crj html,json,txt client/build gs://content-stage-mdn/main + gsutil -q -m -h "Cache-Control: public, max-age=86400" cp -r client/build/static gs://${{ vars.GCP_BUCKET_NAME }}/main/static + gsutil -q -m -h "Cache-Control: public, max-age=86400" rsync -cdrj html,json,txt client/build gs://${{ vars.GCP_BUCKET_NAME }}/main # Deploy Function - name: Authenticate with GCP @@ -364,7 +365,7 @@ jobs: --timeout=30s \ --set-env-vars="ORIGIN_MAIN=developer.allizom.org" \ --set-env-vars="ORIGIN_LIVE_SAMPLES=live-samples.mdn.allizom.net" \ - --set-env-vars="SOURCE_CONTENT=https://storage.googleapis.com/content-stage-mdn/main/" \ + --set-env-vars="SOURCE_CONTENT=https://storage.googleapis.com/${{ vars.GCP_BUCKET_NAME }}/main/" \ --set-env-vars="SOURCE_RUMBA=https://api.developer.allizom.org/" \ --set-env-vars="SENTRY_DSN=${{ secrets.SENTRY_DSN_CLOUD_FUNCTION }}" \ --set-env-vars="SENTRY_ENVIRONMENT=stage" \ From 506d89c669bc651e3e60aebd2534f0068ffa0391 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 14 Apr 2023 11:13:25 +0200 Subject: [PATCH 265/343] ci(xyz-build): extract invalidate job --- .github/workflows/xyz-build.yml | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index cd0ea638c7cc..fcdfb5376758 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -18,13 +18,14 @@ on: required: true WIP_PROJECT_ID: required: true + +permissions: + contents: read + id-token: write + jobs: build: environment: xyz - permissions: - contents: read - id-token: write - runs-on: ubuntu-latest-4core env: @@ -228,7 +229,22 @@ jobs: wait $pid done + invalidate: + environment: xyz + needs: build + if: ${{ ! vars.SKIP_INVALIDATE }} + + steps: + - name: Authenticate with GCP + uses: google-github-actions/auth@v0 + with: + token_format: access_token + service_account: deploy-xyz-yari@${{ secrets.GCP_PROJECT_NAME }}.iam.gserviceaccount.com + workload_identity_provider: projects/${{ secrets.WIP_PROJECT_ID }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions + + - name: Setup gcloud + uses: google-github-actions/setup-gcloud@v1 + - name: Invalidate CDN - if: ${{ ! vars.SKIP_INVALIDATE }} run: |- - gcloud compute url-maps invalidate-cdn-cache ${{ vars.GCP_LOAD_BALANCER_NAME }} --path "/*" + gcloud compute url-maps invalidate-cdn-cache ${{ vars.GCP_LOAD_BALANCER_NAME }} --path "/*" \ No newline at end of file From aa1e02370896f0f8687023b67fab4f581a6d0e3a Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 14 Apr 2023 11:28:19 +0200 Subject: [PATCH 266/343] fixup! ci(xyz-build): extract invalidate job --- .github/workflows/xyz-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index fcdfb5376758..b1116f10b01e 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -233,6 +233,7 @@ jobs: environment: xyz needs: build if: ${{ ! vars.SKIP_INVALIDATE }} + runs-on: ubuntu-latest steps: - name: Authenticate with GCP From 9b35f7eae0dda08f20f549fe25575f90bd81eb10 Mon Sep 17 00:00:00 2001 From: Florian Dieminger Date: Fri, 14 Apr 2023 12:06:30 +0200 Subject: [PATCH 267/343] migrage(pong): use pong internal lib in gcp --- gcp/function/package-lock.json | 21 ++++ gcp/function/package.json | 1 + gcp/function/src/handlers/kevel.ts | 163 +++++------------------------ 3 files changed, 48 insertions(+), 137 deletions(-) diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index 37480cfa9726..552fae6a5bec 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -15,6 +15,7 @@ "@yari-internal/constants": "file:src/internal/constants", "@yari-internal/fundamental-redirects": "file:src/internal/fundamental-redirects", "@yari-internal/locale-utils": "file:src/internal/locale-utils", + "@yari-internal/pong": "file:src/internal/pong", "@yari-internal/slug-utils": "file:src/internal/slug-utils", "accept-language-parser": "^1.5.0", "compression": "^1.7.4", @@ -633,6 +634,10 @@ "resolved": "src/internal/locale-utils", "link": true }, + "node_modules/@yari-internal/pong": { + "resolved": "src/internal/pong", + "link": true + }, "node_modules/@yari-internal/slug-utils": { "resolved": "src/internal/slug-utils", "link": true @@ -1592,6 +1597,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -3350,6 +3363,14 @@ "cookie": "^0.5.0" } }, + "src/internal/pong": { + "version": "0.0.1", + "license": "MPL-2.0", + "dependencies": { + "@adzerk/decision-sdk": "^1.0.0-beta.20", + "he": "^1.2.0" + } + }, "src/internal/slug-utils": { "name": "@yari-internal/slug-utils", "version": "0.0.1", diff --git a/gcp/function/package.json b/gcp/function/package.json index 040a79e99736..a99b0d821372 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -32,6 +32,7 @@ "@yari-internal/constants": "file:src/internal/constants", "@yari-internal/fundamental-redirects": "file:src/internal/fundamental-redirects", "@yari-internal/locale-utils": "file:src/internal/locale-utils", + "@yari-internal/pong": "file:src/internal/pong", "@yari-internal/slug-utils": "file:src/internal/slug-utils", "accept-language-parser": "^1.5.0", "compression": "^1.7.4", diff --git a/gcp/function/src/handlers/kevel.ts b/gcp/function/src/handlers/kevel.ts index 1c53ebb2262d..20c0ec9fc008 100644 --- a/gcp/function/src/handlers/kevel.ts +++ b/gcp/function/src/handlers/kevel.ts @@ -1,59 +1,33 @@ -/* global fetch */ -import { createHmac } from "node:crypto"; import * as url from "node:url"; import type express from "express"; import { Client } from "@adzerk/decision-sdk"; + +import { Coder } from "../internal/pong/index.js"; import { - KEVEL_SITE_ID, - KEVEL_NETWORK_ID, - SIGN_SECRET, - CARBON_ZONE_KEY, - CARBON_FALLBACK_ENABLED, -} from "../env.js"; -import { CC_TO_IP } from "../constants.js"; + createPongGetHandler, + createPongClickHandler, + createPongViewedHandler, + fetchImage, +} from "../internal/pong/index.js"; + +import * as env from "../env.js"; + import { getRequestCountry } from "../utils.js"; +const { KEVEL_SITE_ID, KEVEL_NETWORK_ID, SIGN_SECRET } = env; + const siteId = KEVEL_SITE_ID; const networkId = KEVEL_NETWORK_ID; const client = new Client({ networkId, siteId }); -export async function fetchImage(src: string) { - const imageResponse = await fetch(src); - const imageBuffer = await imageResponse.arrayBuffer(); - const contentType = imageResponse.headers.get("content-type"); - return { buf: imageBuffer, contentType }; -} - -function encodeAndSign(s: string): string { - const hmac = createHmac("sha256", SIGN_SECRET); - hmac.update(s); - return `${Buffer.from(s, "utf-8").toString("base64")}.${hmac.digest( - "base64" - )}`; -} - -function decodeAndVerify(tuple: string): string | null { - if (tuple === null) { - return null; - } - const [encoded, digest] = tuple.split("."); - if (!encoded || !digest) { - return null; - } - const s = Buffer.from(encoded, "base64").toString("utf-8"); - const hmac = createHmac("sha256", SIGN_SECRET); - hmac.update(s); - if (hmac.digest("base64") == digest) { - // === won't work... - return s; - } - return null; -} +const coder = new Coder(SIGN_SECRET); +const handleGet = createPongGetHandler(client, coder, env); +const handleClick = createPongClickHandler(coder); +const handleViewed = createPongViewedHandler(coder); export async function proxyKevel(req: express.Request, res: express.Response) { const countryCode = getRequestCountry(req); - const anonymousIp = CC_TO_IP[countryCode] ?? "127.0.0.1"; const userAgent = req.headers["user-agent"] ?? null; @@ -66,68 +40,15 @@ export async function proxyKevel(req: express.Request, res: express.Response) { return res.status(405).end(); } - const { keywords = [] } = req.body; - const decisionReq = { - placements: [{ adTypes: [465, 369] }], - keywords: [...keywords, countryCode], - }; - - const decisionRes = await client.decisions.get(decisionReq, { - ip: anonymousIp, - } as any); - const { decisions: { div0 } = {} } = decisionRes; - if (div0 === null || div0?.[0] === null) { - return res.status(204).end(); - } - - let payload = {}; - - const [{ contents, clickUrl, impressionUrl }] = div0 as any; - if ( - CARBON_FALLBACK_ENABLED && - CARBON_ZONE_KEY && - CARBON_ZONE_KEY !== "undefined" && - contents?.[0]?.data?.customData?.fallback - ) { - // fall back to carbon - try { - const { - ads: [ - { description = null, statlink, statimp, smallImage, ad_via_link }, - ] = [], - } = await ( - await fetch( - `https://srv.buysellads.com/ads/${CARBON_ZONE_KEY}.json?forwardedip=${encodeURIComponent( - anonymousIp - )}${userAgent ? `&useragent=${encodeURIComponent(userAgent)}` : ""}` - ) - ).json(); - payload = { - click: encodeAndSign(clickUrl), - view: encodeAndSign(impressionUrl), - fallback: { - click: encodeAndSign(statlink), - view: encodeAndSign(statimp), - image: encodeAndSign(smallImage), - copy: description, - by: ad_via_link, - }, - }; - } catch (e) { - console.log(e); - return res.status(400).end(); - } - } else { - payload = { - copy: contents?.[0]?.data?.title || "This is an ad without copy?!", - image: encodeAndSign(contents[0]?.data?.imageUrl), - click: encodeAndSign(clickUrl), - view: encodeAndSign(impressionUrl), - }; - } + const body = JSON.parse(Buffer.from(req.body.data, "base64").toString()); + const { statusCode: status, payload } = await handleGet( + body, + countryCode, + userAgent + ); return res - .status(200) + .status(status) .setHeader("cache-control", "no-store") .setHeader("content-type", "application/json") .end(JSON.stringify(payload)); @@ -137,24 +58,7 @@ export async function proxyKevel(req: express.Request, res: express.Response) { } const params = new URLSearchParams(search); try { - const click = decodeAndVerify(params.get("code") ?? ""); - const fallback = decodeAndVerify(params.get("fallback") ?? ""); - - if (!click) { - return res.status(400).end(); - } - - const fetchRes = await fetch(click, { redirect: "manual" }); - let status = fetchRes.status; - let headers = fetchRes.headers; - if (fallback) { - const fallbackRes = await fetch(`https:${fallback}`, { - redirect: "manual", - }); - status = fallbackRes.status; - headers = fallbackRes.headers; - } - const location = headers.get("location"); + const { status, location } = await handleClick(params); if (location && (status === 301 || status === 302)) { return res.redirect(location); } else { @@ -162,7 +66,6 @@ export async function proxyKevel(req: express.Request, res: express.Response) { } } catch (e) { console.error(e); - return res.status(500).end(); } } else if (pathname === "/pong/viewed") { if (req.method !== "POST") { @@ -173,29 +76,15 @@ export async function proxyKevel(req: express.Request, res: express.Response) { } const params = new URLSearchParams(search); try { - const view = decodeAndVerify(params.get("code") ?? ""); - const fallback = decodeAndVerify(params.get("fallback") ?? ""); - fallback && (await fetch(`https:${fallback}`, { redirect: "manual" })); - - if (!view) { - return res.status(400).end(); - } - - await fetch(view, { redirect: "manual" }); + await handleViewed(params); return res.status(201).end(); } catch (e) { console.error(e); - return res.status(500).end(); } } else if (pathname.startsWith("/pimg/")) { - const src = decodeAndVerify( + const src = coder.decodeAndVerify( decodeURIComponent(pathname.substring("/pimg/".length)) ); - - if (!src) { - return res.status(400).end(); - } - const { buf, contentType } = await fetchImage(src); return res .status(200) From 83a186bc529d24cce7cfe9d083ea5a407fc9fd1c Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 14 Apr 2023 12:14:24 +0200 Subject: [PATCH 268/343] chore(libs/pong): add TypeScript declarations `npx tsc *.js --declaration --emitDeclarationOnly --allowJs` --- libs/pong/cc2ip.d.ts | 1 + libs/pong/click.d.ts | 4 ++++ libs/pong/coding.d.ts | 14 ++++++++++++++ libs/pong/fallback.d.ts | 12 ++++++++++++ libs/pong/get.d.ts | 12 ++++++++++++ libs/pong/image.d.ts | 4 ++++ libs/pong/index.d.ts | 6 ++++++ libs/pong/viewed.d.ts | 3 +++ 8 files changed, 56 insertions(+) create mode 100644 libs/pong/cc2ip.d.ts create mode 100644 libs/pong/click.d.ts create mode 100644 libs/pong/coding.d.ts create mode 100644 libs/pong/fallback.d.ts create mode 100644 libs/pong/get.d.ts create mode 100644 libs/pong/image.d.ts create mode 100644 libs/pong/index.d.ts create mode 100644 libs/pong/viewed.d.ts diff --git a/libs/pong/cc2ip.d.ts b/libs/pong/cc2ip.d.ts new file mode 100644 index 000000000000..2083d68c6971 --- /dev/null +++ b/libs/pong/cc2ip.d.ts @@ -0,0 +1 @@ +export default function anonymousIpByCC(countryCode: any): any; diff --git a/libs/pong/click.d.ts b/libs/pong/click.d.ts new file mode 100644 index 000000000000..a2879f650887 --- /dev/null +++ b/libs/pong/click.d.ts @@ -0,0 +1,4 @@ +export function createPongClickHandler(coder: any): (params: any) => Promise<{ + status: number; + location: string; +}>; diff --git a/libs/pong/coding.d.ts b/libs/pong/coding.d.ts new file mode 100644 index 000000000000..0801ab828788 --- /dev/null +++ b/libs/pong/coding.d.ts @@ -0,0 +1,14 @@ +export class Coder { + /** + * Create a Coder to en/decode and sign/verify fields. + * @param {string} signSecret - The signing secret. + */ + constructor(signSecret: string); + /** + * The signing secret. + * @type {string} + */ + signSecret: string; + encodeAndSign(s?: string): string; + decodeAndVerify(tuple?: string): string; +} diff --git a/libs/pong/fallback.d.ts b/libs/pong/fallback.d.ts new file mode 100644 index 000000000000..e34e885e24aa --- /dev/null +++ b/libs/pong/fallback.d.ts @@ -0,0 +1,12 @@ +export function fallbackHandler( + coder: any, + carbonZoneKey: any, + userAgent: any, + anonymousIp: any +): Promise<{ + click: any; + view: any; + image: any; + copy: any; + by: any; +}>; diff --git a/libs/pong/get.d.ts b/libs/pong/get.d.ts new file mode 100644 index 000000000000..3a2c52d5107d --- /dev/null +++ b/libs/pong/get.d.ts @@ -0,0 +1,12 @@ +export function createPongGetHandler( + client: any, + coder: any, + env: any +): ( + body: any, + countryCode: any, + userAgent: any +) => Promise<{ + statusCode: number; + payload: any; +}>; diff --git a/libs/pong/image.d.ts b/libs/pong/image.d.ts new file mode 100644 index 000000000000..1a73e63fd414 --- /dev/null +++ b/libs/pong/image.d.ts @@ -0,0 +1,4 @@ +export function fetchImage(src: any): Promise<{ + buf: ArrayBuffer; + contentType: string; +}>; diff --git a/libs/pong/index.d.ts b/libs/pong/index.d.ts new file mode 100644 index 000000000000..9cd2405e9b84 --- /dev/null +++ b/libs/pong/index.d.ts @@ -0,0 +1,6 @@ +export * from "./coding.js"; +export * from "./get.js"; +export * from "./image.js"; +export * from "./click.js"; +export * from "./viewed.js"; +export * from "./fallback.js"; diff --git a/libs/pong/viewed.d.ts b/libs/pong/viewed.d.ts new file mode 100644 index 000000000000..27572c6bcbf1 --- /dev/null +++ b/libs/pong/viewed.d.ts @@ -0,0 +1,3 @@ +export function createPongViewedHandler( + coder: any +): (params: any) => Promise; From 4d36e1bcf03beec2d97efb5dfde62ea7bc677d06 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 14 Apr 2023 12:28:03 +0200 Subject: [PATCH 269/343] chore(libs/pong): improve TypeScript definitions --- libs/pong/cc2ip.d.ts | 2 +- libs/pong/click.d.ts | 6 +++++- libs/pong/fallback.d.ts | 20 +++++++++++--------- libs/pong/get.d.ts | 36 +++++++++++++++++++++++++++++------- libs/pong/image.d.ts | 2 +- libs/pong/viewed.d.ts | 6 ++++-- 6 files changed, 51 insertions(+), 21 deletions(-) diff --git a/libs/pong/cc2ip.d.ts b/libs/pong/cc2ip.d.ts index 2083d68c6971..6fa02cc67380 100644 --- a/libs/pong/cc2ip.d.ts +++ b/libs/pong/cc2ip.d.ts @@ -1 +1 @@ -export default function anonymousIpByCC(countryCode: any): any; +export default function anonymousIpByCC(countryCode: string): string; diff --git a/libs/pong/click.d.ts b/libs/pong/click.d.ts index a2879f650887..532d9f1bc90c 100644 --- a/libs/pong/click.d.ts +++ b/libs/pong/click.d.ts @@ -1,4 +1,8 @@ -export function createPongClickHandler(coder: any): (params: any) => Promise<{ +import { Coder } from "./coding.js"; + +export function createPongClickHandler(coder: Coder): ( + params: URLSearchParams +) => Promise<{ status: number; location: string; }>; diff --git a/libs/pong/fallback.d.ts b/libs/pong/fallback.d.ts index e34e885e24aa..be648df1cc60 100644 --- a/libs/pong/fallback.d.ts +++ b/libs/pong/fallback.d.ts @@ -1,12 +1,14 @@ +import { Coder } from "./coding.js"; + export function fallbackHandler( - coder: any, - carbonZoneKey: any, - userAgent: any, - anonymousIp: any + coder: Coder, + carbonZoneKey: string, + userAgent: string, + anonymousIp: string ): Promise<{ - click: any; - view: any; - image: any; - copy: any; - by: any; + click: string; + view: string; + image: string; + copy: string; + by: string; }>; diff --git a/libs/pong/get.d.ts b/libs/pong/get.d.ts index 3a2c52d5107d..638e6e41e5be 100644 --- a/libs/pong/get.d.ts +++ b/libs/pong/get.d.ts @@ -1,12 +1,34 @@ +import { Client } from "@adzerk/decision-sdk"; +import { Coder } from "./coding.js"; + +type Payload = + | { + click: string; + view: string; + fallback: { + click: string; + view: string; + image: string; + copy: string; + by: string; + }; + } + | { + copy: string; + image: string; + click: string; + view: string; + }; + export function createPongGetHandler( - client: any, - coder: any, - env: any + client: Client, + coder: Coder, + env: { KEVEL_SITE_ID: number; KEVEL_NETWORK_ID: number; SIGN_SECRET: string } ): ( - body: any, - countryCode: any, - userAgent: any + body: string, + countryCode: string, + userAgent: string ) => Promise<{ statusCode: number; - payload: any; + payload: Payload; }>; diff --git a/libs/pong/image.d.ts b/libs/pong/image.d.ts index 1a73e63fd414..415a38bbd895 100644 --- a/libs/pong/image.d.ts +++ b/libs/pong/image.d.ts @@ -1,4 +1,4 @@ -export function fetchImage(src: any): Promise<{ +export function fetchImage(src: string): Promise<{ buf: ArrayBuffer; contentType: string; }>; diff --git a/libs/pong/viewed.d.ts b/libs/pong/viewed.d.ts index 27572c6bcbf1..48c1d38fc77b 100644 --- a/libs/pong/viewed.d.ts +++ b/libs/pong/viewed.d.ts @@ -1,3 +1,5 @@ +import { Coder } from "./coding.js"; + export function createPongViewedHandler( - coder: any -): (params: any) => Promise; + coder: Coder +): (params: URLSearchParams) => Promise; From c350dbb7b05b7ba8f6a3c67c51ce4000195dfe0e Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 14 Apr 2023 12:32:38 +0200 Subject: [PATCH 270/343] chore(gcp/function): update package-lock.json --- gcp/function/package-lock.json | 1 + 1 file changed, 1 insertion(+) diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index 552fae6a5bec..296befede40c 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -3364,6 +3364,7 @@ } }, "src/internal/pong": { + "name": "@yari-internal/pong", "version": "0.0.1", "license": "MPL-2.0", "dependencies": { From a7ba03090cb234082959eb76882e3805933c8cb0 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 14 Apr 2023 12:34:39 +0200 Subject: [PATCH 271/343] chore(gcp/function): avoid userAgent being null --- gcp/function/src/handlers/kevel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/function/src/handlers/kevel.ts b/gcp/function/src/handlers/kevel.ts index 20c0ec9fc008..be5184c8e688 100644 --- a/gcp/function/src/handlers/kevel.ts +++ b/gcp/function/src/handlers/kevel.ts @@ -29,7 +29,7 @@ const handleViewed = createPongViewedHandler(coder); export async function proxyKevel(req: express.Request, res: express.Response) { const countryCode = getRequestCountry(req); - const userAgent = req.headers["user-agent"] ?? null; + const userAgent = req.headers["user-agent"] ?? ""; const parsedUrl = url.parse(req.url); const pathname = parsedUrl.pathname ?? ""; From 6aedf7db683641d769004e6a27f99f0d0f5871ed Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 14 Apr 2023 12:40:20 +0200 Subject: [PATCH 272/343] fix(gcp/function): use body, as body-parser already parses it --- gcp/function/src/handlers/kevel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp/function/src/handlers/kevel.ts b/gcp/function/src/handlers/kevel.ts index be5184c8e688..2de9d88a6d82 100644 --- a/gcp/function/src/handlers/kevel.ts +++ b/gcp/function/src/handlers/kevel.ts @@ -40,7 +40,7 @@ export async function proxyKevel(req: express.Request, res: express.Response) { return res.status(405).end(); } - const body = JSON.parse(Buffer.from(req.body.data, "base64").toString()); + const { body } = req; const { statusCode: status, payload } = await handleGet( body, countryCode, From 6ed449da082913f9eb47c074f1959ac61ba3c09e Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 14 Apr 2023 12:46:20 +0200 Subject: [PATCH 273/343] ci(stage-build): extract invalidate job --- .github/workflows/stage-build.yml | 68 +++++++++++++++++++------------ 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 527d0bf68a99..8733ad746162 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -55,13 +55,13 @@ on: WIP_PROJECT_ID: required: true +permissions: + contents: read + id-token: write + jobs: build: environment: stage - permissions: - contents: read - id-token: write - runs-on: ubuntu-latest # Only run the scheduled workflows on the main repo. @@ -288,21 +288,6 @@ jobs: poetry run deployer update-lambda-functions ./aws-lambda poetry run deployer search-index ../client/build - - name: Configure AWS Credentials - if: ${{ ! (vars.SKIP_INVALIDATE || vars.SKIP_AWS) }} - uses: aws-actions/configure-aws-credentials@v1-node16 - with: - aws-access-key-id: ${{ secrets.DEPLOYER_STAGE_AND_DEV_AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.DEPLOYER_STAGE_AND_DEV_AWS_SECRET_ACCESS_KEY }} - aws-region: us-east-1 - - - name: Invalidate CDN - if: ${{ ! (vars.SKIP_INVALIDATE || vars.SKIP_AWS) }} - env: - DISTRIBUTION: E2MLRMA1VTVDHX - PATHS: /* - run: aws cloudfront create-invalidation --distribution-id "$DISTRIBUTION" --paths "$PATHS" - - name: Authenticate with GCP if: ${{ ! vars.SKIP_BUILD }} uses: google-github-actions/auth@v1 @@ -323,7 +308,7 @@ jobs: # Deploy Function - name: Authenticate with GCP - if: ${{ ! vars.SKIP_FUNCTION || !vars.SKIP_INVALIDATE }} + if: ${{ ! vars.SKIP_FUNCTION }} uses: google-github-actions/auth@v1 with: token_format: access_token @@ -331,7 +316,7 @@ jobs: workload_identity_provider: projects/${{ secrets.WIP_PROJECT_ID }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions - name: Setup gcloud - if: ${{ ! vars.SKIP_FUNCTION || !vars.SKIP_INVALIDATE }} + if: ${{ ! vars.SKIP_FUNCTION }} uses: google-github-actions/setup-gcloud@v1 with: install_components: "beta" @@ -384,11 +369,6 @@ jobs: wait $pid done - - name: Invalidate CDN - if: ${{ ! vars.SKIP_INVALIDATE }} - run: |- - gcloud compute url-maps invalidate-cdn-cache ${{ secrets.GCP_LOAD_BALANCER_NAME }} --path "/*" - - name: Slack Notification if: failure() uses: rtCamp/action-slack-notify@v2 @@ -400,3 +380,39 @@ jobs: SLACK_MESSAGE: "Build failed :collision:" SLACK_FOOTER: "Powered by stage-build.yml" SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + + invalidate: + environment: stage + needs: build + if: ${{ ! vars.SKIP_INVALIDATE }} + runs-on: ubuntu-latest + + steps: + - name: Configure AWS Credentials + if: ${{ ! vars.SKIP_AWS }} + uses: aws-actions/configure-aws-credentials@v1-node16 + with: + aws-access-key-id: ${{ secrets.DEPLOYER_STAGE_AND_DEV_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.DEPLOYER_STAGE_AND_DEV_AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Invalidate CDN + if: ${{ ! vars.SKIP_AWS }} + env: + DISTRIBUTION: E2MLRMA1VTVDHX + PATHS: /* + run: aws cloudfront create-invalidation --distribution-id "$DISTRIBUTION" --paths "$PATHS" + + - name: Authenticate with GCP + uses: google-github-actions/auth@v1 + with: + token_format: access_token + service_account: deploy-stage-nonprod-mdn-ingre@${{ secrets.GCP_PROJECT_NAME }}.iam.gserviceaccount.com + workload_identity_provider: projects/${{ secrets.WIP_PROJECT_ID }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions + + - name: Setup gcloud + uses: google-github-actions/setup-gcloud@v1 + + - name: Invalidate CDN + run: |- + gcloud compute url-maps invalidate-cdn-cache ${{ secrets.GCP_LOAD_BALANCER_NAME }} --path "/*" \ No newline at end of file From a39cc9c4d3049b2306e0a317b72415d107a9b245 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 14 Apr 2023 12:50:43 +0200 Subject: [PATCH 274/343] chore(gcp/function): remove @adzerk/decision-sdk from deps This is a dependency of `@yari-internal/pong`. --- gcp/function/package-lock.json | 1 - gcp/function/package.json | 1 - 2 files changed, 2 deletions(-) diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index 296befede40c..8b4aa54fc148 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -9,7 +9,6 @@ "version": "0.0.1", "license": "MPL-2.0", "dependencies": { - "@adzerk/decision-sdk": "^1.0.0-beta.20", "@google-cloud/functions-framework": "^3.1.3", "@sentry/serverless": "^7.47.0", "@yari-internal/constants": "file:src/internal/constants", diff --git a/gcp/function/package.json b/gcp/function/package.json index a99b0d821372..d029d1767c84 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -26,7 +26,6 @@ ] }, "dependencies": { - "@adzerk/decision-sdk": "^1.0.0-beta.20", "@google-cloud/functions-framework": "^3.1.3", "@sentry/serverless": "^7.47.0", "@yari-internal/constants": "file:src/internal/constants", From 90d0916b5e744ba10173979e8a19f07e1672b2d5 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 14 Apr 2023 14:17:09 +0200 Subject: [PATCH 275/343] Revert "chore(gcp/function): remove @adzerk/decision-sdk from deps" This reverts commit a39cc9c4d3049b2306e0a317b72415d107a9b245. --- gcp/function/package-lock.json | 1 + gcp/function/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/gcp/function/package-lock.json b/gcp/function/package-lock.json index 8b4aa54fc148..296befede40c 100644 --- a/gcp/function/package-lock.json +++ b/gcp/function/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "MPL-2.0", "dependencies": { + "@adzerk/decision-sdk": "^1.0.0-beta.20", "@google-cloud/functions-framework": "^3.1.3", "@sentry/serverless": "^7.47.0", "@yari-internal/constants": "file:src/internal/constants", diff --git a/gcp/function/package.json b/gcp/function/package.json index d029d1767c84..a99b0d821372 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -26,6 +26,7 @@ ] }, "dependencies": { + "@adzerk/decision-sdk": "^1.0.0-beta.20", "@google-cloud/functions-framework": "^3.1.3", "@sentry/serverless": "^7.47.0", "@yari-internal/constants": "file:src/internal/constants", From 7660a82a129b8b4fc7c80351c3aa2ab327498e81 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 14 Apr 2023 14:44:52 +0200 Subject: [PATCH 276/343] style: lint .github/workflows/*.yml --- .github/workflows/stage-build.yml | 2 +- .github/workflows/xyz-build.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 8733ad746162..d2d434539011 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -415,4 +415,4 @@ jobs: - name: Invalidate CDN run: |- - gcloud compute url-maps invalidate-cdn-cache ${{ secrets.GCP_LOAD_BALANCER_NAME }} --path "/*" \ No newline at end of file + gcloud compute url-maps invalidate-cdn-cache ${{ secrets.GCP_LOAD_BALANCER_NAME }} --path "/*" diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index b1116f10b01e..f7e48a3bdc72 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -248,4 +248,4 @@ jobs: - name: Invalidate CDN run: |- - gcloud compute url-maps invalidate-cdn-cache ${{ vars.GCP_LOAD_BALANCER_NAME }} --path "/*" \ No newline at end of file + gcloud compute url-maps invalidate-cdn-cache ${{ vars.GCP_LOAD_BALANCER_NAME }} --path "/*" From a4a051f539fd92ab37294f1a9d0279738bddfc3e Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 14 Apr 2023 15:43:12 +0200 Subject: [PATCH 277/343] ci(prod-build): extract invalidate job --- .github/workflows/prod-build.yml | 68 +++++++++++++++++++------------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/.github/workflows/prod-build.yml b/.github/workflows/prod-build.yml index 6c8ce0f40f42..dcff63fe3688 100644 --- a/.github/workflows/prod-build.yml +++ b/.github/workflows/prod-build.yml @@ -49,13 +49,13 @@ on: WIP_PROJECT_ID: required: true +permissions: + contents: read + id-token: write + jobs: build: environment: prod - permissions: - contents: read - id-token: write - runs-on: ubuntu-latest # Only run the scheduled workflows on the main repo. @@ -285,21 +285,6 @@ jobs: poetry run deployer update-lambda-functions ./aws-lambda poetry run deployer search-index ../client/build - - name: Configure AWS Credentials - if: ${{ ! (vars.SKIP_INVALIDATE || vars.SKIP_AWS) }} - uses: aws-actions/configure-aws-credentials@v1-node16 - with: - aws-access-key-id: ${{ secrets.DEPLOYER_PROD_AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.DEPLOYER_PROD_AWS_SECRET_ACCESS_KEY }} - aws-region: us-east-1 - - - name: Invalidate CDN - if: ${{ ! (vars.SKIP_INVALIDATE || vars.SKIP_AWS) }} - env: - DISTRIBUTION: E2ZY2DGUN70EMI - PATHS: /* - run: aws cloudfront create-invalidation --distribution-id "$DISTRIBUTION" --paths "$PATHS" - - name: Authenticate with GCP if: ${{ ! vars.SKIP_BUILD }} uses: google-github-actions/auth@v1 @@ -318,9 +303,8 @@ jobs: gsutil -q -m -h "Cache-Control: public, max-age=86400" cp -r client/build/static gs://${{ vars.GCP_BUCKET_NAME }}/main/static gsutil -q -m -h "Cache-Control: public, max-age=86400" rsync -cdrj html,json,txt client/build gs://${{ vars.GCP_BUCKET_NAME }}/main - # Deploy Function - name: Authenticate with GCP - if: ${{ ! vars.SKIP_FUNCTION || !vars.SKIP_INVALIDATE }} + if: ${{ ! vars.SKIP_FUNCTION }} uses: google-github-actions/auth@v1 with: token_format: access_token @@ -328,7 +312,7 @@ jobs: workload_identity_provider: projects/${{ secrets.WIP_PROJECT_ID }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions - name: Setup gcloud - if: ${{ ! vars.SKIP_FUNCTION || !vars.SKIP_INVALIDATE }} + if: ${{ ! vars.SKIP_FUNCTION }} uses: google-github-actions/setup-gcloud@v1 with: install_components: "beta" @@ -381,11 +365,6 @@ jobs: wait $pid done - - name: Invalidate CDN - if: ${{ ! vars.SKIP_INVALIDATE }} - run: |- - gcloud compute url-maps invalidate-cdn-cache ${{ secrets.GCP_LOAD_BALANCER_NAME }} --path "/*" - - name: Slack Notification if: failure() uses: rtCamp/action-slack-notify@v2 @@ -397,3 +376,38 @@ jobs: SLACK_MESSAGE: "Build failed :collision:" SLACK_FOOTER: "Powered by prod-build.yml" SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + + invalidate: + environment: prod + needs: build + if: ${{ ! vars.SKIP_INVALIDATE }} + runs-on: ubuntu-latest + + steps: + - name: Configure AWS Credentials + if: ${{ ! vars.SKIP_AWS }} + uses: aws-actions/configure-aws-credentials@v1-node16 + with: + aws-access-key-id: ${{ secrets.DEPLOYER_PROD_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.DEPLOYER_PROD_AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Invalidate AWS CloudFront CDN + if: ${{ ! vars.SKIP_AWS }} + env: + DISTRIBUTION: E2ZY2DGUN70EMI + PATHS: /* + run: aws cloudfront create-invalidation --distribution-id "$DISTRIBUTION" --paths "$PATHS" + + - name: Authenticate with GCP + uses: google-github-actions/auth@v1 + with: + token_format: access_token + service_account: deploy-prod-prod-mdn-ingress@${{ secrets.GCP_PROJECT_NAME }}.iam.gserviceaccount.com + workload_identity_provider: projects/${{ secrets.WIP_PROJECT_ID }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions + + - name: Setup gcloud + uses: google-github-actions/setup-gcloud@v1 + + - name: Invalidate Google Cloud CDN + run: gcloud compute url-maps invalidate-cdn-cache ${{ secrets.GCP_LOAD_BALANCER_NAME }} --path "/*" \ No newline at end of file From 4a13e058ce33b11ca4d3b0ed0976530ae98e2892 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 14 Apr 2023 20:37:49 +0200 Subject: [PATCH 278/343] fixup! ci(prod-build): extract invalidate job --- .github/workflows/prod-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prod-build.yml b/.github/workflows/prod-build.yml index dcff63fe3688..4d4185f46370 100644 --- a/.github/workflows/prod-build.yml +++ b/.github/workflows/prod-build.yml @@ -410,4 +410,4 @@ jobs: uses: google-github-actions/setup-gcloud@v1 - name: Invalidate Google Cloud CDN - run: gcloud compute url-maps invalidate-cdn-cache ${{ secrets.GCP_LOAD_BALANCER_NAME }} --path "/*" \ No newline at end of file + run: gcloud compute url-maps invalidate-cdn-cache ${{ secrets.GCP_LOAD_BALANCER_NAME }} --path "/*" From 9575c91feb55c0f4db9757f649af091309b18210 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 14 Apr 2023 20:38:58 +0200 Subject: [PATCH 279/343] style(prettier): ignore /gcp/function/src/internal --- .prettierignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.prettierignore b/.prettierignore index 4694145ed555..d51e1301919b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -29,3 +29,4 @@ _githistory.json /mdn/content popularities.json /client/public/service-worker.js +/gcp/function/src/internal/ From 51a7b478d9769312f591d9bc493d94953a9022e3 Mon Sep 17 00:00:00 2001 From: Florian Dieminger Date: Mon, 17 Apr 2023 12:19:29 +0200 Subject: [PATCH 280/343] Update gcp/function/src/handlers/kevel.ts Co-authored-by: Claas Augner <495429+caugner@users.noreply.github.com> --- gcp/function/src/handlers/kevel.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/gcp/function/src/handlers/kevel.ts b/gcp/function/src/handlers/kevel.ts index 2de9d88a6d82..b3ca3b75c341 100644 --- a/gcp/function/src/handlers/kevel.ts +++ b/gcp/function/src/handlers/kevel.ts @@ -69,10 +69,7 @@ export async function proxyKevel(req: express.Request, res: express.Response) { } } else if (pathname === "/pong/viewed") { if (req.method !== "POST") { - return { - status: 405, - statusDescription: "METHOD_NOT_ALLOWED", - }; + return res.status(405).end(); } const params = new URLSearchParams(search); try { From dfdbbf1e5b2d13a244eb75377791b8755280ae88 Mon Sep 17 00:00:00 2001 From: Claas Augner <495429+caugner@users.noreply.github.com> Date: Mon, 17 Apr 2023 14:21:12 +0200 Subject: [PATCH 281/343] Update libs/pong/coding.d.ts `decodeAndVerify` may return null. Co-authored-by: Florian Dieminger --- libs/pong/coding.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/pong/coding.d.ts b/libs/pong/coding.d.ts index 0801ab828788..1bb0d7efcd8d 100644 --- a/libs/pong/coding.d.ts +++ b/libs/pong/coding.d.ts @@ -10,5 +10,5 @@ export class Coder { */ signSecret: string; encodeAndSign(s?: string): string; - decodeAndVerify(tuple?: string): string; + decodeAndVerify(tuple?: string): string | null; } From a7c755c0e8d45ee2230faa807926b7bf29587b78 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 15:18:45 +0200 Subject: [PATCH 282/343] fix(gcp/function): check against null --- gcp/function/src/handlers/kevel.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gcp/function/src/handlers/kevel.ts b/gcp/function/src/handlers/kevel.ts index b3ca3b75c341..769a9c9bd0ab 100644 --- a/gcp/function/src/handlers/kevel.ts +++ b/gcp/function/src/handlers/kevel.ts @@ -82,6 +82,9 @@ export async function proxyKevel(req: express.Request, res: express.Response) { const src = coder.decodeAndVerify( decodeURIComponent(pathname.substring("/pimg/".length)) ); + if (!src) { + return res.status(400).end(); + } const { buf, contentType } = await fetchImage(src); return res .status(200) From eea0d463a3644224ec0c1848557a944c7ba80520 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 15:20:19 +0200 Subject: [PATCH 283/343] chore(libs/locale-utils): remove package-lock.json This causes `yarn install` and `npm i` to be run both, and both modify it to a slightly different version. --- libs/locale-utils/package-lock.json | 32 ----------------------------- 1 file changed, 32 deletions(-) delete mode 100644 libs/locale-utils/package-lock.json diff --git a/libs/locale-utils/package-lock.json b/libs/locale-utils/package-lock.json deleted file mode 100644 index 97db7867fc2a..000000000000 --- a/libs/locale-utils/package-lock.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "@yari-internal/locale-utils", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@yari-internal/locale-utils", - "version": "0.0.1", - "license": "MPL-2.0", - "dependencies": { - "accept-language-parser": "^1.5.0", - "cookie": "^0.5.0" - } - }, - "node_modules/accept-language-parser": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/accept-language-parser/-/accept-language-parser-1.5.0.tgz", - "integrity": "sha512-QhyTbMLYo0BBGg1aWbeMG4ekWtds/31BrEU+DONOg/7ax23vxpL03Pb7/zBmha2v7vdD3AyzZVWBVGEZxKOXWw==", - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - } - } -} From 50b993c7bca12172efc108a2eea26e478892cf58 Mon Sep 17 00:00:00 2001 From: Florian Dieminger Date: Mon, 17 Apr 2023 17:01:10 +0200 Subject: [PATCH 284/343] fix(plans): call res.end() for fix timeout --- gcp/function/src/handlers/plans.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gcp/function/src/handlers/plans.ts b/gcp/function/src/handlers/plans.ts index 4d082e84af83..6fdcb945365e 100644 --- a/gcp/function/src/handlers/plans.ts +++ b/gcp/function/src/handlers/plans.ts @@ -26,7 +26,7 @@ export async function plans(req: express.Request, res: express.Response) { const supportedCurrency = lookupData.countryToCurrency[countryCode]; if (!supportedCurrency) { - return res.status(404); + return res.status(404).end(); } const acceptLanguage = typeof localeHeader === "string" ? localeHeader : null; @@ -48,7 +48,7 @@ export async function plans(req: express.Request, res: express.Response) { const plans = lookupData.langCurrencyToPlans[key]; if (!plans) { - return res.status(500); + return res.status(500).end(); } const planResult: PlanResult = {}; From c38a3e0be343c0c0297e30708f153ae773a88fb4 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 17:37:07 +0200 Subject: [PATCH 285/343] ci(stage-build): remove on.push --- .github/workflows/stage-build.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 3f7f71f74ba6..3d8a38d1e966 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -18,12 +18,6 @@ on: schedule: # * is a special character in YAML so you have to quote this string - cron: "0 */24 * * *" - push: - branches: - - gcp - paths: - - .github/workflows/stage-build.yml - - gcp/function/** workflow_dispatch: inputs: From 53b79fc282b5f0456f402111ccf0a5faae19fd99 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 17:37:39 +0200 Subject: [PATCH 286/343] ci(xyz-build): build on push to main only --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index f7e48a3bdc72..410a48e779b1 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -3,7 +3,7 @@ name: XYZ Build (GCP) on: push: branches: - - gcp + - main paths: - .github/workflows/xyz-build.yml - gcp/function/** From 4ac61b8fbc47a1e573be6ef63700f45db6719af4 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 17:41:58 +0200 Subject: [PATCH 287/343] refactor(gcp/function): rename {redirects => build-redirects} --- .github/workflows/prod-build.yml | 2 +- .github/workflows/stage-build.yml | 2 +- .github/workflows/xyz-build.yml | 2 +- gcp/function/package.json | 2 +- gcp/function/src/{build.ts => build-redirects.ts} | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename gcp/function/src/{build.ts => build-redirects.ts} (100%) diff --git a/.github/workflows/prod-build.yml b/.github/workflows/prod-build.yml index e89e75385fac..1af85c79c29f 100644 --- a/.github/workflows/prod-build.yml +++ b/.github/workflows/prod-build.yml @@ -324,7 +324,7 @@ jobs: CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files run: |- npm ci - npm run redirects + npm run build-redirects - name: Deploy Function if: ${{ ! vars.SKIP_FUNCTION }} diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 3d8a38d1e966..fdfa13b9aa47 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -322,7 +322,7 @@ jobs: CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files run: | npm ci - npm run redirects + npm run build-redirects - name: Deploy Function if: ${{ ! vars.SKIP_FUNCTION }} diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 410a48e779b1..d0a3ddc69c7d 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -189,7 +189,7 @@ jobs: CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files run: | npm ci - npm run redirects + npm run build-redirects - name: Deploy Function if: ${{ ! vars.SKIP_FUNCTION }} diff --git a/gcp/function/package.json b/gcp/function/package.json index a99b0d821372..85989127bc2c 100644 --- a/gcp/function/package.json +++ b/gcp/function/package.json @@ -9,11 +9,11 @@ "main": "src/index.js", "scripts": { "build": "tsc -b", + "build-redirects": "ts-node src/build-redirects.ts", "copy-internal": "rm -rf ./src/internal && cp -R ../../libs ./src/internal", "gcp-build": "npm run build", "prepare": "([ ! -e ../../libs ] || npm run copy-internal)", "proxy": "ts-node src/proxy.ts", - "redirects": "ts-node src/build.ts", "server": "npm run build && functions-framework --target=mdnHandler", "server:watch": "nodemon --exec npm run server", "start": "nf start" diff --git a/gcp/function/src/build.ts b/gcp/function/src/build-redirects.ts similarity index 100% rename from gcp/function/src/build.ts rename to gcp/function/src/build-redirects.ts From 3d0a92a8be3eeb3a09bf4042917816d4ce7dc8a9 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 18:55:45 +0200 Subject: [PATCH 288/343] refactor(gcp/function): move {gcp/function => cloud-function} --- .eslintignore | 2 +- .github/workflows/prod-build.yml | 4 ++-- .github/workflows/stage-build.yml | 4 ++-- .github/workflows/xyz-build.yml | 6 +++--- .prettierignore | 2 +- {gcp/function => cloud-function}/.gcloudignore | 0 {gcp/function => cloud-function}/.gitignore | 0 {gcp/function => cloud-function}/Procfile | 0 {gcp/function => cloud-function}/package-lock.json | 0 {gcp/function => cloud-function}/package.json | 0 {gcp/function => cloud-function}/src/app.ts | 0 {gcp/function => cloud-function}/src/build-redirects.ts | 0 {gcp/function => cloud-function}/src/constants.ts | 0 {gcp/function => cloud-function}/src/env.ts | 0 {gcp/function => cloud-function}/src/handlers/content.ts | 0 {gcp/function => cloud-function}/src/handlers/kevel.ts | 0 {gcp/function => cloud-function}/src/handlers/plans.ts | 0 {gcp/function => cloud-function}/src/handlers/rumba.ts | 0 {gcp/function => cloud-function}/src/handlers/telemetry.ts | 0 {gcp/function => cloud-function}/src/headers.ts | 0 {gcp/function => cloud-function}/src/index.ts | 0 .../function => cloud-function}/src/middlewares/notFound.ts | 0 .../src/middlewares/pathnameLC.ts | 0 .../src/middlewares/redirectFundamental.ts | 0 .../src/middlewares/redirectLeadingSlash.ts | 0 .../src/middlewares/redirectLocale.ts | 0 .../src/middlewares/redirectMovedPages.ts | 0 .../src/middlewares/redirectTrailingSlash.ts | 0 .../src/middlewares/requireOrigin.ts | 0 .../src/middlewares/resolveIndexHTML.ts | 0 {gcp/function => cloud-function}/src/plans/index.ts | 0 {gcp/function => cloud-function}/src/plans/prod.ts | 0 {gcp/function => cloud-function}/src/plans/stage.ts | 0 {gcp/function => cloud-function}/src/proxy.ts | 0 {gcp/function => cloud-function}/src/utils.ts | 0 {gcp/function => cloud-function}/tsconfig.json | 0 36 files changed, 9 insertions(+), 9 deletions(-) rename {gcp/function => cloud-function}/.gcloudignore (100%) rename {gcp/function => cloud-function}/.gitignore (100%) rename {gcp/function => cloud-function}/Procfile (100%) rename {gcp/function => cloud-function}/package-lock.json (100%) rename {gcp/function => cloud-function}/package.json (100%) rename {gcp/function => cloud-function}/src/app.ts (100%) rename {gcp/function => cloud-function}/src/build-redirects.ts (100%) rename {gcp/function => cloud-function}/src/constants.ts (100%) rename {gcp/function => cloud-function}/src/env.ts (100%) rename {gcp/function => cloud-function}/src/handlers/content.ts (100%) rename {gcp/function => cloud-function}/src/handlers/kevel.ts (100%) rename {gcp/function => cloud-function}/src/handlers/plans.ts (100%) rename {gcp/function => cloud-function}/src/handlers/rumba.ts (100%) rename {gcp/function => cloud-function}/src/handlers/telemetry.ts (100%) rename {gcp/function => cloud-function}/src/headers.ts (100%) rename {gcp/function => cloud-function}/src/index.ts (100%) rename {gcp/function => cloud-function}/src/middlewares/notFound.ts (100%) rename {gcp/function => cloud-function}/src/middlewares/pathnameLC.ts (100%) rename {gcp/function => cloud-function}/src/middlewares/redirectFundamental.ts (100%) rename {gcp/function => cloud-function}/src/middlewares/redirectLeadingSlash.ts (100%) rename {gcp/function => cloud-function}/src/middlewares/redirectLocale.ts (100%) rename {gcp/function => cloud-function}/src/middlewares/redirectMovedPages.ts (100%) rename {gcp/function => cloud-function}/src/middlewares/redirectTrailingSlash.ts (100%) rename {gcp/function => cloud-function}/src/middlewares/requireOrigin.ts (100%) rename {gcp/function => cloud-function}/src/middlewares/resolveIndexHTML.ts (100%) rename {gcp/function => cloud-function}/src/plans/index.ts (100%) rename {gcp/function => cloud-function}/src/plans/prod.ts (100%) rename {gcp/function => cloud-function}/src/plans/stage.ts (100%) rename {gcp/function => cloud-function}/src/proxy.ts (100%) rename {gcp/function => cloud-function}/src/utils.ts (100%) rename {gcp/function => cloud-function}/tsconfig.json (100%) diff --git a/.eslintignore b/.eslintignore index 4e14d9da4f5a..049caec3ab47 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,6 +7,6 @@ client/public/service-worker.js client/public/ client/src/document/*.js filecheck/*.js -gcp/function/src/internal/ +cloud-function/src/internal/ mdn/content/ tool/*.js diff --git a/.github/workflows/prod-build.yml b/.github/workflows/prod-build.yml index 1af85c79c29f..5841d871c2ba 100644 --- a/.github/workflows/prod-build.yml +++ b/.github/workflows/prod-build.yml @@ -318,7 +318,7 @@ jobs: - name: Generate redirects map if: ${{ ! vars.SKIP_FUNCTION }} - working-directory: gcp/function + working-directory: cloud-function env: CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files @@ -334,7 +334,7 @@ jobs: --gen2 \ --runtime=nodejs18 \ --region=$region \ - --source=gcp/function \ + --source=cloud-function \ --trigger-http \ --allow-unauthenticated \ --entry-point=mdnHandler \ diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index fdfa13b9aa47..78d9960eb52e 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -316,7 +316,7 @@ jobs: - name: Generate redirects map if: ${{ ! vars.SKIP_FUNCTION }} - working-directory: gcp/function + working-directory: cloud-function env: CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files @@ -332,7 +332,7 @@ jobs: --gen2 \ --runtime=nodejs18 \ --region=$region \ - --source=gcp/function \ + --source=cloud-function \ --trigger-http \ --allow-unauthenticated \ --entry-point=mdnHandler \ diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index d0a3ddc69c7d..6288d4fb03e4 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -6,7 +6,7 @@ on: - main paths: - .github/workflows/xyz-build.yml - - gcp/function/** + - cloud-function/** workflow_dispatch: @@ -183,7 +183,7 @@ jobs: - name: Generate redirects map if: ${{ ! vars.SKIP_FUNCTION }} - working-directory: gcp/function + working-directory: cloud-function env: CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files @@ -199,7 +199,7 @@ jobs: --gen2 \ --runtime=nodejs18 \ --region=$region \ - --source=gcp/function \ + --source=cloud-function \ --trigger-http \ --allow-unauthenticated \ --entry-point=mdnHandler \ diff --git a/.prettierignore b/.prettierignore index d51e1301919b..6fca7089be7a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -29,4 +29,4 @@ _githistory.json /mdn/content popularities.json /client/public/service-worker.js -/gcp/function/src/internal/ +/cloud-function/src/internal/ diff --git a/gcp/function/.gcloudignore b/cloud-function/.gcloudignore similarity index 100% rename from gcp/function/.gcloudignore rename to cloud-function/.gcloudignore diff --git a/gcp/function/.gitignore b/cloud-function/.gitignore similarity index 100% rename from gcp/function/.gitignore rename to cloud-function/.gitignore diff --git a/gcp/function/Procfile b/cloud-function/Procfile similarity index 100% rename from gcp/function/Procfile rename to cloud-function/Procfile diff --git a/gcp/function/package-lock.json b/cloud-function/package-lock.json similarity index 100% rename from gcp/function/package-lock.json rename to cloud-function/package-lock.json diff --git a/gcp/function/package.json b/cloud-function/package.json similarity index 100% rename from gcp/function/package.json rename to cloud-function/package.json diff --git a/gcp/function/src/app.ts b/cloud-function/src/app.ts similarity index 100% rename from gcp/function/src/app.ts rename to cloud-function/src/app.ts diff --git a/gcp/function/src/build-redirects.ts b/cloud-function/src/build-redirects.ts similarity index 100% rename from gcp/function/src/build-redirects.ts rename to cloud-function/src/build-redirects.ts diff --git a/gcp/function/src/constants.ts b/cloud-function/src/constants.ts similarity index 100% rename from gcp/function/src/constants.ts rename to cloud-function/src/constants.ts diff --git a/gcp/function/src/env.ts b/cloud-function/src/env.ts similarity index 100% rename from gcp/function/src/env.ts rename to cloud-function/src/env.ts diff --git a/gcp/function/src/handlers/content.ts b/cloud-function/src/handlers/content.ts similarity index 100% rename from gcp/function/src/handlers/content.ts rename to cloud-function/src/handlers/content.ts diff --git a/gcp/function/src/handlers/kevel.ts b/cloud-function/src/handlers/kevel.ts similarity index 100% rename from gcp/function/src/handlers/kevel.ts rename to cloud-function/src/handlers/kevel.ts diff --git a/gcp/function/src/handlers/plans.ts b/cloud-function/src/handlers/plans.ts similarity index 100% rename from gcp/function/src/handlers/plans.ts rename to cloud-function/src/handlers/plans.ts diff --git a/gcp/function/src/handlers/rumba.ts b/cloud-function/src/handlers/rumba.ts similarity index 100% rename from gcp/function/src/handlers/rumba.ts rename to cloud-function/src/handlers/rumba.ts diff --git a/gcp/function/src/handlers/telemetry.ts b/cloud-function/src/handlers/telemetry.ts similarity index 100% rename from gcp/function/src/handlers/telemetry.ts rename to cloud-function/src/handlers/telemetry.ts diff --git a/gcp/function/src/headers.ts b/cloud-function/src/headers.ts similarity index 100% rename from gcp/function/src/headers.ts rename to cloud-function/src/headers.ts diff --git a/gcp/function/src/index.ts b/cloud-function/src/index.ts similarity index 100% rename from gcp/function/src/index.ts rename to cloud-function/src/index.ts diff --git a/gcp/function/src/middlewares/notFound.ts b/cloud-function/src/middlewares/notFound.ts similarity index 100% rename from gcp/function/src/middlewares/notFound.ts rename to cloud-function/src/middlewares/notFound.ts diff --git a/gcp/function/src/middlewares/pathnameLC.ts b/cloud-function/src/middlewares/pathnameLC.ts similarity index 100% rename from gcp/function/src/middlewares/pathnameLC.ts rename to cloud-function/src/middlewares/pathnameLC.ts diff --git a/gcp/function/src/middlewares/redirectFundamental.ts b/cloud-function/src/middlewares/redirectFundamental.ts similarity index 100% rename from gcp/function/src/middlewares/redirectFundamental.ts rename to cloud-function/src/middlewares/redirectFundamental.ts diff --git a/gcp/function/src/middlewares/redirectLeadingSlash.ts b/cloud-function/src/middlewares/redirectLeadingSlash.ts similarity index 100% rename from gcp/function/src/middlewares/redirectLeadingSlash.ts rename to cloud-function/src/middlewares/redirectLeadingSlash.ts diff --git a/gcp/function/src/middlewares/redirectLocale.ts b/cloud-function/src/middlewares/redirectLocale.ts similarity index 100% rename from gcp/function/src/middlewares/redirectLocale.ts rename to cloud-function/src/middlewares/redirectLocale.ts diff --git a/gcp/function/src/middlewares/redirectMovedPages.ts b/cloud-function/src/middlewares/redirectMovedPages.ts similarity index 100% rename from gcp/function/src/middlewares/redirectMovedPages.ts rename to cloud-function/src/middlewares/redirectMovedPages.ts diff --git a/gcp/function/src/middlewares/redirectTrailingSlash.ts b/cloud-function/src/middlewares/redirectTrailingSlash.ts similarity index 100% rename from gcp/function/src/middlewares/redirectTrailingSlash.ts rename to cloud-function/src/middlewares/redirectTrailingSlash.ts diff --git a/gcp/function/src/middlewares/requireOrigin.ts b/cloud-function/src/middlewares/requireOrigin.ts similarity index 100% rename from gcp/function/src/middlewares/requireOrigin.ts rename to cloud-function/src/middlewares/requireOrigin.ts diff --git a/gcp/function/src/middlewares/resolveIndexHTML.ts b/cloud-function/src/middlewares/resolveIndexHTML.ts similarity index 100% rename from gcp/function/src/middlewares/resolveIndexHTML.ts rename to cloud-function/src/middlewares/resolveIndexHTML.ts diff --git a/gcp/function/src/plans/index.ts b/cloud-function/src/plans/index.ts similarity index 100% rename from gcp/function/src/plans/index.ts rename to cloud-function/src/plans/index.ts diff --git a/gcp/function/src/plans/prod.ts b/cloud-function/src/plans/prod.ts similarity index 100% rename from gcp/function/src/plans/prod.ts rename to cloud-function/src/plans/prod.ts diff --git a/gcp/function/src/plans/stage.ts b/cloud-function/src/plans/stage.ts similarity index 100% rename from gcp/function/src/plans/stage.ts rename to cloud-function/src/plans/stage.ts diff --git a/gcp/function/src/proxy.ts b/cloud-function/src/proxy.ts similarity index 100% rename from gcp/function/src/proxy.ts rename to cloud-function/src/proxy.ts diff --git a/gcp/function/src/utils.ts b/cloud-function/src/utils.ts similarity index 100% rename from gcp/function/src/utils.ts rename to cloud-function/src/utils.ts diff --git a/gcp/function/tsconfig.json b/cloud-function/tsconfig.json similarity index 100% rename from gcp/function/tsconfig.json rename to cloud-function/tsconfig.json From 0d8799c44c96cc8ba24b27f4f4ca0861ccf53760 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 18:57:09 +0200 Subject: [PATCH 289/343] fixup! refactor(gcp/function): move {gcp/function => cloud-function} --- {gcp => cloud-function}/README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {gcp => cloud-function}/README.md (100%) diff --git a/gcp/README.md b/cloud-function/README.md similarity index 100% rename from gcp/README.md rename to cloud-function/README.md From 967ac5dff4f3feb9c594a9c9a2a31e7c56b94ad4 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 18:58:11 +0200 Subject: [PATCH 290/343] fixup! refactor(gcp/function): move {gcp/function => cloud-function} --- .eslintignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintignore b/.eslintignore index 049caec3ab47..1c755258f465 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,7 +6,7 @@ libs/ client/public/service-worker.js client/public/ client/src/document/*.js -filecheck/*.js cloud-function/src/internal/ +filecheck/*.js mdn/content/ tool/*.js From dc594c82b384d408b5d962c2d9bfd5bdaadc36f8 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 19:11:57 +0200 Subject: [PATCH 291/343] fixup! refactor(gcp/function): move {gcp/function => cloud-function} --- cloud-function/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud-function/package.json b/cloud-function/package.json index 85989127bc2c..febddd5214ae 100644 --- a/cloud-function/package.json +++ b/cloud-function/package.json @@ -10,7 +10,7 @@ "scripts": { "build": "tsc -b", "build-redirects": "ts-node src/build-redirects.ts", - "copy-internal": "rm -rf ./src/internal && cp -R ../../libs ./src/internal", + "copy-internal": "rm -rf ./src/internal && cp -R ../libs ./src/internal", "gcp-build": "npm run build", "prepare": "([ ! -e ../../libs ] || npm run copy-internal)", "proxy": "ts-node src/proxy.ts", From 93cb37d3600aad8ed7fdbbc7d3b0aa75a76275fd Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 19:17:12 +0200 Subject: [PATCH 292/343] fixup! refactor(gcp/function): move {gcp/function => cloud-function} --- cloud-function/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud-function/package.json b/cloud-function/package.json index febddd5214ae..6a57aa77a892 100644 --- a/cloud-function/package.json +++ b/cloud-function/package.json @@ -12,7 +12,7 @@ "build-redirects": "ts-node src/build-redirects.ts", "copy-internal": "rm -rf ./src/internal && cp -R ../libs ./src/internal", "gcp-build": "npm run build", - "prepare": "([ ! -e ../../libs ] || npm run copy-internal)", + "prepare": "([ ! -e ../libs ] || npm run copy-internal)", "proxy": "ts-node src/proxy.ts", "server": "npm run build && functions-framework --target=mdnHandler", "server:watch": "nodemon --exec npm run server", From 42705d60c82ddfbd3933e3a05e313dc2bc2a16cd Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 19:29:50 +0200 Subject: [PATCH 293/343] chore(cloud-function): reuse 404 buffer --- cloud-function/src/handlers/content.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cloud-function/src/handlers/content.ts b/cloud-function/src/handlers/content.ts index 0d07e594d33c..4f31280ac8d5 100644 --- a/cloud-function/src/handlers/content.ts +++ b/cloud-function/src/handlers/content.ts @@ -10,6 +10,8 @@ import { Source, sourceUri } from "../env.js"; const NOT_FOUND_PATH = "en-us/_spas/404.html"; +let notFoundBuffer: ArrayBuffer; + export function createContentProxy(): express.Handler { const target = sourceUri(Source.content); return createProxyMiddleware({ @@ -24,9 +26,12 @@ export function createContentProxy(): express.Handler { async (responseBuffer, proxyRes, req, res) => { withContentResponseHeaders(proxyRes, req, res); if (proxyRes.statusCode === 404) { - const response = await fetch(`${target}${NOT_FOUND_PATH}`); + if (!notFoundBuffer) { + const response = await fetch(`${target}${NOT_FOUND_PATH}`); + notFoundBuffer = await response.arrayBuffer(); + } res.setHeader("Content-Type", "text/html"); - return Buffer.from(await response.arrayBuffer()); + return Buffer.from(notFoundBuffer); } return responseBuffer; From 0d96c62a1cc33750146bb351237595fc3cfcb9f8 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 19:32:50 +0200 Subject: [PATCH 294/343] chore(cloud-function): remove unused file --- cloud-function/src/constants.ts | 177 -------------------------------- 1 file changed, 177 deletions(-) delete mode 100644 cloud-function/src/constants.ts diff --git a/cloud-function/src/constants.ts b/cloud-function/src/constants.ts deleted file mode 100644 index 0e3113ae2eb8..000000000000 --- a/cloud-function/src/constants.ts +++ /dev/null @@ -1,177 +0,0 @@ -export const CC_TO_IP: { [countryCode: string]: string } = { - AD: "46.172.232.102", - AE: "94.200.156.82", - AF: "103.146.146.97", - AG: "66.249.150.51", - AL: "192.109.219.158", - AM: "37.186.119.247", - AO: "41.63.188.109", - AQ: "2a05:dfc7:5353::53", - AR: "45.65.225.220", - AS: "202.70.125.9", - AT: "195.234.100.25", - AU: "203.147.94.71", - AZ: "85.132.17.38", - BA: "217.23.199.77", - BB: "64.210.41.182", - BD: "103.245.96.174", - BE: "194.6.227.60", - BF: "41.216.155.125", - BG: "194.12.224.14", - BH: "157.175.58.241", - BJ: "154.66.142.8", - BM: "199.68.195.249", - BN: "61.6.225.234", - BO: "190.186.245.140", - BQ: "190.107.252.178", - BR: "131.72.141.149", - BW: "154.73.39.241", - BY: "195.222.86.106", - BZ: "200.123.208.126", - CA: "23.252.239.1", - CD: "102.68.154.33", - CG: "41.207.125.57", - CH: "34.65.77.163", - CL: "45.7.228.232", - CM: "41.211.108.4", - CN: "223.6.6.195", - CO: "190.156.237.85", - CR: "190.106.79.228", - CU: "152.206.201.137", - CY: "46.199.74.102", - CZ: "188.116.91.246", - DE: "188.68.45.12", - DJ: "196.201.198.165", - DK: "152.115.91.203", - DO: "181.232.190.197", - DZ: "41.100.61.49", - EC: "181.198.11.152", - EE: "188.127.234.193", - EG: "41.176.151.71", - ES: "185.2.68.79", - ET: "196.188.168.146", - FI: "87.92.251.93", - FJ: "202.129.231.250", - FR: "217.111.148.201", - GA: "41.158.1.162", - GB: "87.242.157.62", - GE: "188.169.44.18", - GF: "217.108.102.6", - GH: "102.176.81.182", - GM: "197.148.74.19", - GN: "102.176.160.107", - GP: "81.248.145.154", - GQ: "102.164.255.149", - GR: "2.84.38.146", - GT: "38.52.208.199", - GU: "114.142.243.170", - HK: "202.155.202.75", - HN: "190.185.118.104", - HR: "85.114.40.50", - HU: "79.172.213.213", - ID: "175.103.40.42", - IE: "86.43.125.181", - IL: "31.154.9.62", - IM: "185.246.128.162", - IN: "164.52.223.153", - IQ: "213.32.252.91", - IR: "2.188.21.131", - IS: "193.4.89.2", - IT: "217.70.144.8", - JM: "63.143.125.14", - JO: "92.253.127.65", - JP: "153.182.23.230", - KE: "197.232.155.222", - KG: "92.62.65.237", - KH: "116.212.143.233", - KR: "121.124.124.196", - KW: "195.39.131.78", - KY: "216.144.84.145", - KZ: "87.76.54.213", - LA: "202.137.128.6", - LB: "89.108.139.246", - LK: "203.143.42.24", - LT: "88.119.87.88", - LU: "185.44.142.137", - LV: "86.63.169.194", - LY: "165.16.39.113", - MA: "197.230.103.202", - MD: "212.28.88.241", - ME: "95.155.31.120", - MH: "103.202.148.251", - MK: "146.255.89.51", - MM: "103.115.23.44", - MN: "202.55.190.30", - MO: "182.93.25.100", - MR: "82.151.74.36", - MT: "194.105.32.2", - MU: "41.216.125.179", - MV: "202.1.194.16", - MW: "41.216.228.228", - MX: "189.234.24.31", - MY: "60.51.247.44", - MZ: "197.219.229.86", - NA: "197.234.98.210", - NC: "202.22.148.124", - NG: "41.184.148.252", - NI: "200.62.96.39", - NL: "185.81.8.252", - NO: "80.202.239.184", - NP: "103.126.245.139", - NZ: "121.79.252.57", - OM: "5.21.239.143", - PA: "190.140.202.124", - PE: "200.37.203.90", - PF: "113.197.68.20", - PG: "124.240.199.23", - PH: "124.107.101.26", - PK: "110.39.8.113", - PL: "80.54.219.186", - PR: "12.205.65.7", - PS: "213.6.32.10", - PT: "62.28.205.202", - PW: "202.124.226.133", - PY: "201.217.51.46", - RE: "102.35.162.43", - RO: "188.27.244.73", - RS: "185.248.172.8", - RU: "89.110.59.24", - RW: "41.215.248.143", - SA: "51.211.38.5", - SB: "202.1.172.187", - SC: "185.247.225.17", - SD: "196.1.210.35", - SE: "158.174.37.226", - SG: "119.75.28.242", - SI: "195.230.121.12", - SK: "87.197.154.105", - SN: "213.154.80.203", - SV: "190.87.164.207", - SY: "5.134.255.230", - SZ: "102.215.99.18", - TD: "102.223.194.134", - TG: "41.207.186.166", - TH: "1.4.206.84", - TJ: "85.9.129.36", - TN: "197.15.87.14", - TR: "176.236.129.141", - TT: "200.1.104.36", - TW: "36.237.20.227", - TZ: "41.59.200.123", - UA: "176.126.123.42", - UG: "154.72.199.202", - US: "68.228.28.247", - UY: "190.64.151.74", - UZ: "185.183.242.130", - VE: "190.75.2.41", - VI: "8.26.19.118", - VN: "171.235.173.79", - YE: "134.35.132.212", - YT: "41.242.116.25", - ZA: "102.36.123.181", - ZW: "41.174.104.223", -}; - -export const DEFAULT_COUNTRY = "US"; - -export const THIRTY_DAYS = 3600 * 24 * 30; From ba2fe23cd20f6d578336e977581f48c48a477840 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 19:51:54 +0200 Subject: [PATCH 295/343] chore(cloud-function): remove DEBUG_TELEMETRY --- cloud-function/src/env.ts | 4 ---- cloud-function/src/handlers/telemetry.ts | 5 +---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/cloud-function/src/env.ts b/cloud-function/src/env.ts index 1cc02bbb4b33..fb0d97b96dec 100644 --- a/cloud-function/src/env.ts +++ b/cloud-function/src/env.ts @@ -78,7 +78,3 @@ export const CARBON_FALLBACK_ENABLED = Boolean( // (Use https://github.com/FiloSottile/mkcert to generate a locally-trusted certificate.) export const HTTPS_KEY_FILE = process.env["HTTPS_KEY_FILE"] ?? ""; export const HTTPS_CERT_FILE = process.env["HTTPS_CERT_FILE"] ?? ""; - -export const DEBUG_TELEMETRY = Boolean( - JSON.parse(process.env["DEBUG_TELEMETRY"] || "false") -); diff --git a/cloud-function/src/handlers/telemetry.ts b/cloud-function/src/handlers/telemetry.ts index 916464d6eaaa..940206518f7a 100644 --- a/cloud-function/src/handlers/telemetry.ts +++ b/cloud-function/src/handlers/telemetry.ts @@ -1,10 +1,7 @@ import { createProxyMiddleware, fixRequestBody } from "http-proxy-middleware"; -import { DEBUG_TELEMETRY } from "../env.js"; export const proxyTelemetry = createProxyMiddleware({ - target: DEBUG_TELEMETRY - ? "http://localhost:8888/" - : "https://incoming.telemetry.mozilla.org", + target: "https://incoming.telemetry.mozilla.org", changeOrigin: true, autoRewrite: true, proxyTimeout: 20000, From bde561282ac16b967126b423465427515a27d920 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 20:24:49 +0200 Subject: [PATCH 296/343] Revert "chore(cloud-function): remove unused file" This reverts commit 0d96c62a1cc33750146bb351237595fc3cfcb9f8. --- cloud-function/src/constants.ts | 177 ++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 cloud-function/src/constants.ts diff --git a/cloud-function/src/constants.ts b/cloud-function/src/constants.ts new file mode 100644 index 000000000000..0e3113ae2eb8 --- /dev/null +++ b/cloud-function/src/constants.ts @@ -0,0 +1,177 @@ +export const CC_TO_IP: { [countryCode: string]: string } = { + AD: "46.172.232.102", + AE: "94.200.156.82", + AF: "103.146.146.97", + AG: "66.249.150.51", + AL: "192.109.219.158", + AM: "37.186.119.247", + AO: "41.63.188.109", + AQ: "2a05:dfc7:5353::53", + AR: "45.65.225.220", + AS: "202.70.125.9", + AT: "195.234.100.25", + AU: "203.147.94.71", + AZ: "85.132.17.38", + BA: "217.23.199.77", + BB: "64.210.41.182", + BD: "103.245.96.174", + BE: "194.6.227.60", + BF: "41.216.155.125", + BG: "194.12.224.14", + BH: "157.175.58.241", + BJ: "154.66.142.8", + BM: "199.68.195.249", + BN: "61.6.225.234", + BO: "190.186.245.140", + BQ: "190.107.252.178", + BR: "131.72.141.149", + BW: "154.73.39.241", + BY: "195.222.86.106", + BZ: "200.123.208.126", + CA: "23.252.239.1", + CD: "102.68.154.33", + CG: "41.207.125.57", + CH: "34.65.77.163", + CL: "45.7.228.232", + CM: "41.211.108.4", + CN: "223.6.6.195", + CO: "190.156.237.85", + CR: "190.106.79.228", + CU: "152.206.201.137", + CY: "46.199.74.102", + CZ: "188.116.91.246", + DE: "188.68.45.12", + DJ: "196.201.198.165", + DK: "152.115.91.203", + DO: "181.232.190.197", + DZ: "41.100.61.49", + EC: "181.198.11.152", + EE: "188.127.234.193", + EG: "41.176.151.71", + ES: "185.2.68.79", + ET: "196.188.168.146", + FI: "87.92.251.93", + FJ: "202.129.231.250", + FR: "217.111.148.201", + GA: "41.158.1.162", + GB: "87.242.157.62", + GE: "188.169.44.18", + GF: "217.108.102.6", + GH: "102.176.81.182", + GM: "197.148.74.19", + GN: "102.176.160.107", + GP: "81.248.145.154", + GQ: "102.164.255.149", + GR: "2.84.38.146", + GT: "38.52.208.199", + GU: "114.142.243.170", + HK: "202.155.202.75", + HN: "190.185.118.104", + HR: "85.114.40.50", + HU: "79.172.213.213", + ID: "175.103.40.42", + IE: "86.43.125.181", + IL: "31.154.9.62", + IM: "185.246.128.162", + IN: "164.52.223.153", + IQ: "213.32.252.91", + IR: "2.188.21.131", + IS: "193.4.89.2", + IT: "217.70.144.8", + JM: "63.143.125.14", + JO: "92.253.127.65", + JP: "153.182.23.230", + KE: "197.232.155.222", + KG: "92.62.65.237", + KH: "116.212.143.233", + KR: "121.124.124.196", + KW: "195.39.131.78", + KY: "216.144.84.145", + KZ: "87.76.54.213", + LA: "202.137.128.6", + LB: "89.108.139.246", + LK: "203.143.42.24", + LT: "88.119.87.88", + LU: "185.44.142.137", + LV: "86.63.169.194", + LY: "165.16.39.113", + MA: "197.230.103.202", + MD: "212.28.88.241", + ME: "95.155.31.120", + MH: "103.202.148.251", + MK: "146.255.89.51", + MM: "103.115.23.44", + MN: "202.55.190.30", + MO: "182.93.25.100", + MR: "82.151.74.36", + MT: "194.105.32.2", + MU: "41.216.125.179", + MV: "202.1.194.16", + MW: "41.216.228.228", + MX: "189.234.24.31", + MY: "60.51.247.44", + MZ: "197.219.229.86", + NA: "197.234.98.210", + NC: "202.22.148.124", + NG: "41.184.148.252", + NI: "200.62.96.39", + NL: "185.81.8.252", + NO: "80.202.239.184", + NP: "103.126.245.139", + NZ: "121.79.252.57", + OM: "5.21.239.143", + PA: "190.140.202.124", + PE: "200.37.203.90", + PF: "113.197.68.20", + PG: "124.240.199.23", + PH: "124.107.101.26", + PK: "110.39.8.113", + PL: "80.54.219.186", + PR: "12.205.65.7", + PS: "213.6.32.10", + PT: "62.28.205.202", + PW: "202.124.226.133", + PY: "201.217.51.46", + RE: "102.35.162.43", + RO: "188.27.244.73", + RS: "185.248.172.8", + RU: "89.110.59.24", + RW: "41.215.248.143", + SA: "51.211.38.5", + SB: "202.1.172.187", + SC: "185.247.225.17", + SD: "196.1.210.35", + SE: "158.174.37.226", + SG: "119.75.28.242", + SI: "195.230.121.12", + SK: "87.197.154.105", + SN: "213.154.80.203", + SV: "190.87.164.207", + SY: "5.134.255.230", + SZ: "102.215.99.18", + TD: "102.223.194.134", + TG: "41.207.186.166", + TH: "1.4.206.84", + TJ: "85.9.129.36", + TN: "197.15.87.14", + TR: "176.236.129.141", + TT: "200.1.104.36", + TW: "36.237.20.227", + TZ: "41.59.200.123", + UA: "176.126.123.42", + UG: "154.72.199.202", + US: "68.228.28.247", + UY: "190.64.151.74", + UZ: "185.183.242.130", + VE: "190.75.2.41", + VI: "8.26.19.118", + VN: "171.235.173.79", + YE: "134.35.132.212", + YT: "41.242.116.25", + ZA: "102.36.123.181", + ZW: "41.174.104.223", +}; + +export const DEFAULT_COUNTRY = "US"; + +export const THIRTY_DAYS = 3600 * 24 * 30; From da226cc0d10d8247a595288f26b7fb3a5dce47cf Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 20:25:19 +0200 Subject: [PATCH 297/343] chore(cloud-function): remove unused constnat --- cloud-function/src/constants.ts | 174 -------------------------------- 1 file changed, 174 deletions(-) diff --git a/cloud-function/src/constants.ts b/cloud-function/src/constants.ts index 0e3113ae2eb8..ecff4f27ffbc 100644 --- a/cloud-function/src/constants.ts +++ b/cloud-function/src/constants.ts @@ -1,177 +1,3 @@ -export const CC_TO_IP: { [countryCode: string]: string } = { - AD: "46.172.232.102", - AE: "94.200.156.82", - AF: "103.146.146.97", - AG: "66.249.150.51", - AL: "192.109.219.158", - AM: "37.186.119.247", - AO: "41.63.188.109", - AQ: "2a05:dfc7:5353::53", - AR: "45.65.225.220", - AS: "202.70.125.9", - AT: "195.234.100.25", - AU: "203.147.94.71", - AZ: "85.132.17.38", - BA: "217.23.199.77", - BB: "64.210.41.182", - BD: "103.245.96.174", - BE: "194.6.227.60", - BF: "41.216.155.125", - BG: "194.12.224.14", - BH: "157.175.58.241", - BJ: "154.66.142.8", - BM: "199.68.195.249", - BN: "61.6.225.234", - BO: "190.186.245.140", - BQ: "190.107.252.178", - BR: "131.72.141.149", - BW: "154.73.39.241", - BY: "195.222.86.106", - BZ: "200.123.208.126", - CA: "23.252.239.1", - CD: "102.68.154.33", - CG: "41.207.125.57", - CH: "34.65.77.163", - CL: "45.7.228.232", - CM: "41.211.108.4", - CN: "223.6.6.195", - CO: "190.156.237.85", - CR: "190.106.79.228", - CU: "152.206.201.137", - CY: "46.199.74.102", - CZ: "188.116.91.246", - DE: "188.68.45.12", - DJ: "196.201.198.165", - DK: "152.115.91.203", - DO: "181.232.190.197", - DZ: "41.100.61.49", - EC: "181.198.11.152", - EE: "188.127.234.193", - EG: "41.176.151.71", - ES: "185.2.68.79", - ET: "196.188.168.146", - FI: "87.92.251.93", - FJ: "202.129.231.250", - FR: "217.111.148.201", - GA: "41.158.1.162", - GB: "87.242.157.62", - GE: "188.169.44.18", - GF: "217.108.102.6", - GH: "102.176.81.182", - GM: "197.148.74.19", - GN: "102.176.160.107", - GP: "81.248.145.154", - GQ: "102.164.255.149", - GR: "2.84.38.146", - GT: "38.52.208.199", - GU: "114.142.243.170", - HK: "202.155.202.75", - HN: "190.185.118.104", - HR: "85.114.40.50", - HU: "79.172.213.213", - ID: "175.103.40.42", - IE: "86.43.125.181", - IL: "31.154.9.62", - IM: "185.246.128.162", - IN: "164.52.223.153", - IQ: "213.32.252.91", - IR: "2.188.21.131", - IS: "193.4.89.2", - IT: "217.70.144.8", - JM: "63.143.125.14", - JO: "92.253.127.65", - JP: "153.182.23.230", - KE: "197.232.155.222", - KG: "92.62.65.237", - KH: "116.212.143.233", - KR: "121.124.124.196", - KW: "195.39.131.78", - KY: "216.144.84.145", - KZ: "87.76.54.213", - LA: "202.137.128.6", - LB: "89.108.139.246", - LK: "203.143.42.24", - LT: "88.119.87.88", - LU: "185.44.142.137", - LV: "86.63.169.194", - LY: "165.16.39.113", - MA: "197.230.103.202", - MD: "212.28.88.241", - ME: "95.155.31.120", - MH: "103.202.148.251", - MK: "146.255.89.51", - MM: "103.115.23.44", - MN: "202.55.190.30", - MO: "182.93.25.100", - MR: "82.151.74.36", - MT: "194.105.32.2", - MU: "41.216.125.179", - MV: "202.1.194.16", - MW: "41.216.228.228", - MX: "189.234.24.31", - MY: "60.51.247.44", - MZ: "197.219.229.86", - NA: "197.234.98.210", - NC: "202.22.148.124", - NG: "41.184.148.252", - NI: "200.62.96.39", - NL: "185.81.8.252", - NO: "80.202.239.184", - NP: "103.126.245.139", - NZ: "121.79.252.57", - OM: "5.21.239.143", - PA: "190.140.202.124", - PE: "200.37.203.90", - PF: "113.197.68.20", - PG: "124.240.199.23", - PH: "124.107.101.26", - PK: "110.39.8.113", - PL: "80.54.219.186", - PR: "12.205.65.7", - PS: "213.6.32.10", - PT: "62.28.205.202", - PW: "202.124.226.133", - PY: "201.217.51.46", - RE: "102.35.162.43", - RO: "188.27.244.73", - RS: "185.248.172.8", - RU: "89.110.59.24", - RW: "41.215.248.143", - SA: "51.211.38.5", - SB: "202.1.172.187", - SC: "185.247.225.17", - SD: "196.1.210.35", - SE: "158.174.37.226", - SG: "119.75.28.242", - SI: "195.230.121.12", - SK: "87.197.154.105", - SN: "213.154.80.203", - SV: "190.87.164.207", - SY: "5.134.255.230", - SZ: "102.215.99.18", - TD: "102.223.194.134", - TG: "41.207.186.166", - TH: "1.4.206.84", - TJ: "85.9.129.36", - TN: "197.15.87.14", - TR: "176.236.129.141", - TT: "200.1.104.36", - TW: "36.237.20.227", - TZ: "41.59.200.123", - UA: "176.126.123.42", - UG: "154.72.199.202", - US: "68.228.28.247", - UY: "190.64.151.74", - UZ: "185.183.242.130", - VE: "190.75.2.41", - VI: "8.26.19.118", - VN: "171.235.173.79", - YE: "134.35.132.212", - YT: "41.242.116.25", - ZA: "102.36.123.181", - ZW: "41.174.104.223", -}; - export const DEFAULT_COUNTRY = "US"; export const THIRTY_DAYS = 3600 * 24 * 30; From bdb49bdb0705eae291c1cd5dda1f817d90e30c54 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 21:10:52 +0200 Subject: [PATCH 298/343] feat(cloud-function): default to serving client/build --- cloud-function/package-lock.json | 260 +++++++++++++++++++++++++++++++ cloud-function/package.json | 2 + cloud-function/src/env.ts | 14 +- cloud-function/src/proxy.ts | 23 ++- 4 files changed, 288 insertions(+), 11 deletions(-) diff --git a/cloud-function/package-lock.json b/cloud-function/package-lock.json index 296befede40c..9d9957007575 100644 --- a/cloud-function/package-lock.json +++ b/cloud-function/package-lock.json @@ -29,7 +29,9 @@ "@types/accept-language-parser": "^1.5.3", "@types/compression": "^1.7.2", "@types/http-proxy": "^1.17.10", + "@types/http-server": "^0.12.1", "http-proxy": "^1.18.1", + "http-server": "^14.1.1", "nodemon": "^2.0.22", "npm-run-all": "^4.1.5", "ts-node": "^10.9.1", @@ -588,6 +590,15 @@ "@types/node": "*" } }, + "node_modules/@types/http-server": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@types/http-server/-/http-server-0.12.1.tgz", + "integrity": "sha512-OJ8zs0o8JuHo92KCCsLq4BqkHPi1+Aj2yoPQXJ18LPUxOA1lqKfgBLtHNAQTwwPzeBqyo+HDkWD91MkfOGvNJg==", + "dev": true, + "dependencies": { + "@types/connect": "*" + } + }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -784,6 +795,15 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -806,6 +826,24 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1037,6 +1075,15 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", + "dev": true, + "engines": { + "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", @@ -1610,6 +1657,18 @@ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -1661,6 +1720,103 @@ } } }, + "node_modules/http-server": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", + "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", + "dev": true, + "dependencies": { + "basic-auth": "^2.0.1", + "chalk": "^4.1.2", + "corser": "^2.0.1", + "he": "^1.2.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy": "^1.18.1", + "mime": "^1.6.0", + "minimist": "^1.2.6", + "opener": "^1.5.1", + "portfinder": "^1.0.28", + "secure-compare": "3.0.1", + "union": "~0.5.0", + "url-join": "^4.0.1" + }, + "bin": { + "http-server": "bin/http-server" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/http-server/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/http-server/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/http-server/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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/http-server/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 + }, + "node_modules/http-server/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/http-server/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -2096,6 +2252,12 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "node_modules/lru_map": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", @@ -2210,6 +2372,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -2436,6 +2610,15 @@ "node": ">= 0.8" } }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -2565,6 +2748,35 @@ "node": ">=4" } }, + "node_modules/portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "dev": true, + "dependencies": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/portfinder/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -2777,6 +2989,12 @@ "truncate-utf8-bytes": "^1.0.0" } }, + "node_modules/secure-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", + "dev": true + }, "node_modules/semver": { "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", @@ -3179,6 +3397,18 @@ "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==" }, + "node_modules/union": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", + "dev": true, + "dependencies": { + "qs": "^6.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -3195,6 +3425,12 @@ "punycode": "^2.1.0" } }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true + }, "node_modules/utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", @@ -3256,6 +3492,30 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/cloud-function/package.json b/cloud-function/package.json index 6a57aa77a892..4a766377178b 100644 --- a/cloud-function/package.json +++ b/cloud-function/package.json @@ -46,7 +46,9 @@ "@types/accept-language-parser": "^1.5.3", "@types/compression": "^1.7.2", "@types/http-proxy": "^1.17.10", + "@types/http-server": "^0.12.1", "http-proxy": "^1.18.1", + "http-server": "^14.1.1", "nodemon": "^2.0.22", "npm-run-all": "^4.1.5", "ts-node": "^10.9.1", diff --git a/cloud-function/src/env.ts b/cloud-function/src/env.ts index fb0d97b96dec..cd722d74f30c 100644 --- a/cloud-function/src/env.ts +++ b/cloud-function/src/env.ts @@ -7,6 +7,9 @@ dotenv.config({ path: path.join(cwd(), process.env["ENV_FILE"] || ".env"), }); +export const LOCAL_BUILD = "http://localhost:8100/"; +export const LOCAL_RUMBA = "http://localhost:8000/"; + export enum Origin { main = "main", liveSamples = "liveSamples", @@ -19,10 +22,9 @@ export enum Source { rumba = "rumba", } -export const ORIGIN_MAIN: string = - process.env["ORIGIN_MAIN"] || "developer.mozilla.org"; +export const ORIGIN_MAIN: string = process.env["ORIGIN_MAIN"] || "localhost"; export const ORIGIN_LIVE_SAMPLES: string = - process.env["ORIGIN_LIVE_SAMPLES"] || "live-samples.mdn.mozilla.net"; + process.env["ORIGIN_LIVE_SAMPLES"] || "localhost"; export function origin(req: express.Request): Origin { switch (req.hostname) { @@ -36,11 +38,11 @@ export function origin(req: express.Request): Origin { } export const SOURCE_CONTENT: string = - process.env["SOURCE_CONTENT"] || "https://developer.mozilla.org"; + process.env["SOURCE_CONTENT"] || LOCAL_BUILD; export const SOURCE_LIVE_SAMPLES: string = - process.env["SOURCE_LIVE_SAMPLES"] || "https://live-samples.mdn.mozilla.net"; + process.env["SOURCE_LIVE_SAMPLES"] || LOCAL_BUILD; export const SOURCE_RUMBA: string = - process.env["SOURCE_RUMBA"] || "https://developer.mozilla.org"; + process.env["SOURCE_RUMBA"] || "http://localhost:8000/"; export function getOriginFromRequest(req: express.Request): Origin { if (req.hostname === ORIGIN_MAIN && !req.path.includes("/_sample_.")) { diff --git a/cloud-function/src/proxy.ts b/cloud-function/src/proxy.ts index 0a7416963017..53951cb363be 100644 --- a/cloud-function/src/proxy.ts +++ b/cloud-function/src/proxy.ts @@ -1,8 +1,25 @@ import { readFileSync } from "node:fs"; import { createServer } from "node:https"; import httpProxy from "http-proxy"; +import httpServer from "http-server"; -import { HTTPS_CERT_FILE, HTTPS_KEY_FILE } from "./env.js"; +import { + HTTPS_CERT_FILE, + HTTPS_KEY_FILE, + LOCAL_BUILD, + SOURCE_CONTENT, + SOURCE_LIVE_SAMPLES, +} from "./env.js"; + +if ([SOURCE_CONTENT, SOURCE_LIVE_SAMPLES].includes(LOCAL_BUILD)) { + const url = new URL(LOCAL_BUILD); + const contentServer = httpServer.createServer({ + root: "../client/build", + }); + contentServer.listen(url.port, () => + console.log(`client/build served on port ${url.port}`) + ); +} if (HTTPS_CERT_FILE && HTTPS_KEY_FILE) { const proxy = httpProxy.createProxyServer({ @@ -38,8 +55,4 @@ if (HTTPS_CERT_FILE && HTTPS_KEY_FILE) { console.log( "Hint: Use mkcert to create a locally-trusted development certificate." ); - // eslint-disable-next-line no-constant-condition - while (true) { - // Nothing. - } } From e06c821eef58bbefa73cdb4c12d52f4f474533e3 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 21:39:47 +0200 Subject: [PATCH 299/343] docs(cloud-function): update README --- cloud-function/README.md | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/cloud-function/README.md b/cloud-function/README.md index c643c7c5eeb5..97fc573bef6a 100644 --- a/cloud-function/README.md +++ b/cloud-function/README.md @@ -1,4 +1,22 @@ -## Setup +# Cloud Function -1. Install _google-cloud-sdk_ (e.g. `brew install google-cloud-sdk`). -2. Run `gcloud auth login`. 3. Run `gcloud config set project `. +This is MDN's HTTP request handler, deployed using [Cloud Functions](https://cloud.google.com/functions/) behind [Cloud CDN](https://cloud.google.com/cdn/). It mostly proxies requests and handles some special routes directly. + +## Environment variables + +The function uses the following environment variables: + +* `ORIGIN_MAIN` (default: `"localhost"`) - The expected `Host` header value for requests to the main site. +* `ORIGIN_LIVE_SAMPLES` (default: `"localhost"`) - The expected `Host` header value for requests to live samples. +* `SOURCE_CONTENT` (default: `"http://localhost:8100"`) - The URL at which the built content is served. +* `SOURCE_RUMBA` (default: `"http://localhost:8000"`) - The URL at which the API is served. + +The placement handler uses the following environment variables: + +* `KEVEL_SITE_ID` (default: `0`) - Required for serving placements via Kevel. +* `KEVEL_NETWORK_ID` (default: `0`) - Required for serving placements via Kevel. +* `SIGN_SECRET` (default: `""`) - Required for serving placements. +* `CARBON_ZONE_KEY` (default: `""`) - Required for serving placements via Carbon. +* `CARBON_FALLBACK_ENABLED` (default: `"false"`) - Whether fallback placements should be served via Carbon. + +You can override the defaults by adding `.env` file with `KEY=value` lines. From ccface595f3af5e9bfeedb34b1b626191f2c0613 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 21:41:22 +0200 Subject: [PATCH 300/343] refactor(cloud-function): rename {plans => stripePlans} --- cloud-function/src/app.ts | 4 ++-- cloud-function/src/handlers/{plans.ts => stripe-plans.ts} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename cloud-function/src/handlers/{plans.ts => stripe-plans.ts} (96%) diff --git a/cloud-function/src/app.ts b/cloud-function/src/app.ts index 934b0e103737..c5423ecb7af1 100644 --- a/cloud-function/src/app.ts +++ b/cloud-function/src/app.ts @@ -6,7 +6,7 @@ import { Origin } from "./env.js"; import { createContentProxy } from "./handlers/content.js"; import { proxyKevel } from "./handlers/kevel.js"; import { proxyRumba } from "./handlers/rumba.js"; -import { plans } from "./handlers/plans.js"; +import { stripePlans } from "./handlers/stripe-plans.js"; import { proxyTelemetry } from "./handlers/telemetry.js"; import { pathnameLC } from "./middlewares/pathnameLC.js"; import { resolveIndexHTML } from "./middlewares/resolveIndexHTML.js"; @@ -22,7 +22,7 @@ const proxyContent = createContentProxy(); const router = Router(); router.use(redirectLeadingSlash); -router.all("/api/v1/stripe/plans", requireOrigin(Origin.main), plans); +router.all("/api/v1/stripe/plans", requireOrigin(Origin.main), stripePlans); router.all("/api/*", requireOrigin(Origin.main), proxyRumba); router.all("/admin-api/*", requireOrigin(Origin.main), proxyRumba); router.all("/events/fxa/*", requireOrigin(Origin.main), proxyRumba); diff --git a/cloud-function/src/handlers/plans.ts b/cloud-function/src/handlers/stripe-plans.ts similarity index 96% rename from cloud-function/src/handlers/plans.ts rename to cloud-function/src/handlers/stripe-plans.ts index 6fdcb945365e..c9abfcb89eca 100644 --- a/cloud-function/src/handlers/plans.ts +++ b/cloud-function/src/handlers/stripe-plans.ts @@ -15,7 +15,7 @@ interface Result { plans: PlanResult; } -export async function plans(req: express.Request, res: express.Response) { +export async function stripePlans(req: express.Request, res: express.Response) { const lookupData = ORIGIN_MAIN === "developer.mozilla.org" ? prodLookup : stageLookup; From e3978bdfcf20c9bcb2691368c8fddc2e69c99bd9 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 21:46:25 +0200 Subject: [PATCH 301/343] refactor(cloud-function): rename middlewares {fooBar => foo-bar} --- cloud-function/src/app.ts | 24 +++++++++---------- .../{pathnameLC.ts => lowercase-pathname.ts} | 2 +- .../middlewares/{notFound.ts => not-found.ts} | 0 ...Fundamental.ts => redirect-fundamental.ts} | 0 ...dingSlash.ts => redirect-leading-slash.ts} | 0 .../{redirectLocale.ts => redirect-locale.ts} | 0 ...tMovedPages.ts => redirect-moved-pages.ts} | 0 ...ingSlash.ts => redirect-trailing-slash.ts} | 0 .../{requireOrigin.ts => require-origin.ts} | 0 ...olveIndexHTML.ts => resolve-index-html.ts} | 0 10 files changed, 13 insertions(+), 13 deletions(-) rename cloud-function/src/middlewares/{pathnameLC.ts => lowercase-pathname.ts} (92%) rename cloud-function/src/middlewares/{notFound.ts => not-found.ts} (100%) rename cloud-function/src/middlewares/{redirectFundamental.ts => redirect-fundamental.ts} (100%) rename cloud-function/src/middlewares/{redirectLeadingSlash.ts => redirect-leading-slash.ts} (100%) rename cloud-function/src/middlewares/{redirectLocale.ts => redirect-locale.ts} (100%) rename cloud-function/src/middlewares/{redirectMovedPages.ts => redirect-moved-pages.ts} (100%) rename cloud-function/src/middlewares/{redirectTrailingSlash.ts => redirect-trailing-slash.ts} (100%) rename cloud-function/src/middlewares/{requireOrigin.ts => require-origin.ts} (100%) rename cloud-function/src/middlewares/{resolveIndexHTML.ts => resolve-index-html.ts} (100%) diff --git a/cloud-function/src/app.ts b/cloud-function/src/app.ts index c5423ecb7af1..63b93f33efc3 100644 --- a/cloud-function/src/app.ts +++ b/cloud-function/src/app.ts @@ -8,15 +8,15 @@ import { proxyKevel } from "./handlers/kevel.js"; import { proxyRumba } from "./handlers/rumba.js"; import { stripePlans } from "./handlers/stripe-plans.js"; import { proxyTelemetry } from "./handlers/telemetry.js"; -import { pathnameLC } from "./middlewares/pathnameLC.js"; -import { resolveIndexHTML } from "./middlewares/resolveIndexHTML.js"; -import { redirectLeadingSlash } from "./middlewares/redirectLeadingSlash.js"; -import { redirectMovedPages } from "./middlewares/redirectMovedPages.js"; -import { redirectFundamental } from "./middlewares/redirectFundamental.js"; -import { redirectLocale } from "./middlewares/redirectLocale.js"; -import { redirectTrailingSlash } from "./middlewares/redirectTrailingSlash.js"; -import { requireOrigin } from "./middlewares/requireOrigin.js"; -import { notFound } from "./middlewares/notFound.js"; +import { lowercasePathname } from "./middlewares/lowercase-pathname.js"; +import { resolveIndexHTML } from "./middlewares/resolve-index-html.js"; +import { redirectLeadingSlash } from "./middlewares/redirect-leading-slash.js"; +import { redirectMovedPages } from "./middlewares/redirect-moved-pages.js"; +import { redirectFundamental } from "./middlewares/redirect-fundamental.js"; +import { redirectLocale } from "./middlewares/redirect-locale.js"; +import { redirectTrailingSlash } from "./middlewares/redirect-trailing-slash.js"; +import { requireOrigin } from "./middlewares/require-origin.js"; +import { notFound } from "./middlewares/not-found.js"; const proxyContent = createContentProxy(); @@ -41,13 +41,13 @@ router.get("/", requireOrigin(Origin.main), redirectLocale); router.get( "/[^/]+/docs/*/_sample_.*.html", requireOrigin(Origin.liveSamples), - pathnameLC, + lowercasePathname, proxyContent ); router.get( "/[^/]+/docs/*/*.(png|jpeg|jpg|gif|svg|webp)", requireOrigin(Origin.main, Origin.liveSamples), - pathnameLC, + lowercasePathname, proxyContent ); router.get( @@ -63,7 +63,7 @@ router.get( router.get( "/[^/]+/search-index.json", requireOrigin(Origin.main), - pathnameLC, + lowercasePathname, proxyContent ); router.get( diff --git a/cloud-function/src/middlewares/pathnameLC.ts b/cloud-function/src/middlewares/lowercase-pathname.ts similarity index 92% rename from cloud-function/src/middlewares/pathnameLC.ts rename to cloud-function/src/middlewares/lowercase-pathname.ts index fc325c7d727b..f992ba6395fa 100644 --- a/cloud-function/src/middlewares/pathnameLC.ts +++ b/cloud-function/src/middlewares/lowercase-pathname.ts @@ -2,7 +2,7 @@ import * as url from "node:url"; import type express from "express"; -export async function pathnameLC( +export async function lowercasePathname( req: express.Request, _res: express.Response, next: express.NextFunction diff --git a/cloud-function/src/middlewares/notFound.ts b/cloud-function/src/middlewares/not-found.ts similarity index 100% rename from cloud-function/src/middlewares/notFound.ts rename to cloud-function/src/middlewares/not-found.ts diff --git a/cloud-function/src/middlewares/redirectFundamental.ts b/cloud-function/src/middlewares/redirect-fundamental.ts similarity index 100% rename from cloud-function/src/middlewares/redirectFundamental.ts rename to cloud-function/src/middlewares/redirect-fundamental.ts diff --git a/cloud-function/src/middlewares/redirectLeadingSlash.ts b/cloud-function/src/middlewares/redirect-leading-slash.ts similarity index 100% rename from cloud-function/src/middlewares/redirectLeadingSlash.ts rename to cloud-function/src/middlewares/redirect-leading-slash.ts diff --git a/cloud-function/src/middlewares/redirectLocale.ts b/cloud-function/src/middlewares/redirect-locale.ts similarity index 100% rename from cloud-function/src/middlewares/redirectLocale.ts rename to cloud-function/src/middlewares/redirect-locale.ts diff --git a/cloud-function/src/middlewares/redirectMovedPages.ts b/cloud-function/src/middlewares/redirect-moved-pages.ts similarity index 100% rename from cloud-function/src/middlewares/redirectMovedPages.ts rename to cloud-function/src/middlewares/redirect-moved-pages.ts diff --git a/cloud-function/src/middlewares/redirectTrailingSlash.ts b/cloud-function/src/middlewares/redirect-trailing-slash.ts similarity index 100% rename from cloud-function/src/middlewares/redirectTrailingSlash.ts rename to cloud-function/src/middlewares/redirect-trailing-slash.ts diff --git a/cloud-function/src/middlewares/requireOrigin.ts b/cloud-function/src/middlewares/require-origin.ts similarity index 100% rename from cloud-function/src/middlewares/requireOrigin.ts rename to cloud-function/src/middlewares/require-origin.ts diff --git a/cloud-function/src/middlewares/resolveIndexHTML.ts b/cloud-function/src/middlewares/resolve-index-html.ts similarity index 100% rename from cloud-function/src/middlewares/resolveIndexHTML.ts rename to cloud-function/src/middlewares/resolve-index-html.ts From 77fcb6bdc2f726e72a47361a8f98f2c2b8a63a8b Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 21:50:32 +0200 Subject: [PATCH 302/343] refactor(cloud-function): rename handlers --- cloud-function/src/app.ts | 24 +++++++++++-------- cloud-function/src/env.ts | 4 ++-- ...stripe-plans.ts => handle-stripe-plans.ts} | 5 +++- .../src/handlers/{rumba.ts => proxy-api.ts} | 4 ++-- .../handlers/{content.ts => proxy-content.ts} | 0 .../src/handlers/{kevel.ts => proxy-kevel.ts} | 0 .../{telemetry.ts => proxy-telemetry.ts} | 0 7 files changed, 22 insertions(+), 15 deletions(-) rename cloud-function/src/handlers/{stripe-plans.ts => handle-stripe-plans.ts} (96%) rename cloud-function/src/handlers/{rumba.ts => proxy-api.ts} (74%) rename cloud-function/src/handlers/{content.ts => proxy-content.ts} (100%) rename cloud-function/src/handlers/{kevel.ts => proxy-kevel.ts} (100%) rename cloud-function/src/handlers/{telemetry.ts => proxy-telemetry.ts} (100%) diff --git a/cloud-function/src/app.ts b/cloud-function/src/app.ts index 63b93f33efc3..2231e51fd74c 100644 --- a/cloud-function/src/app.ts +++ b/cloud-function/src/app.ts @@ -3,11 +3,11 @@ import { Router } from "express"; import compression from "compression"; import { Origin } from "./env.js"; -import { createContentProxy } from "./handlers/content.js"; -import { proxyKevel } from "./handlers/kevel.js"; -import { proxyRumba } from "./handlers/rumba.js"; -import { stripePlans } from "./handlers/stripe-plans.js"; -import { proxyTelemetry } from "./handlers/telemetry.js"; +import { createContentProxy } from "./handlers/proxy-content.js"; +import { proxyKevel } from "./handlers/proxy-kevel.js"; +import { proxyApi } from "./handlers/proxy-api.js"; +import { handleStripePlans } from "./handlers/handle-stripe-plans.js"; +import { proxyTelemetry } from "./handlers/proxy-telemetry.js"; import { lowercasePathname } from "./middlewares/lowercase-pathname.js"; import { resolveIndexHTML } from "./middlewares/resolve-index-html.js"; import { redirectLeadingSlash } from "./middlewares/redirect-leading-slash.js"; @@ -22,11 +22,15 @@ const proxyContent = createContentProxy(); const router = Router(); router.use(redirectLeadingSlash); -router.all("/api/v1/stripe/plans", requireOrigin(Origin.main), stripePlans); -router.all("/api/*", requireOrigin(Origin.main), proxyRumba); -router.all("/admin-api/*", requireOrigin(Origin.main), proxyRumba); -router.all("/events/fxa/*", requireOrigin(Origin.main), proxyRumba); -router.all("/users/fxa/*", requireOrigin(Origin.main), proxyRumba); +router.all( + "/api/v1/stripe/plans", + requireOrigin(Origin.main), + handleStripePlans +); +router.all("/api/*", requireOrigin(Origin.main), proxyApi); +router.all("/admin-api/*", requireOrigin(Origin.main), proxyApi); +router.all("/events/fxa/*", requireOrigin(Origin.main), proxyApi); +router.all("/users/fxa/*", requireOrigin(Origin.main), proxyApi); router.all( "/submit/mdn-yari/*", requireOrigin(Origin.main), diff --git a/cloud-function/src/env.ts b/cloud-function/src/env.ts index cd722d74f30c..aee55a52bd01 100644 --- a/cloud-function/src/env.ts +++ b/cloud-function/src/env.ts @@ -19,7 +19,7 @@ export enum Origin { export enum Source { content = "content", liveSamples = "liveSamples", - rumba = "rumba", + api = "rumba", } export const ORIGIN_MAIN: string = process.env["ORIGIN_MAIN"] || "localhost"; @@ -60,7 +60,7 @@ export function sourceUri(source: Source): string { return SOURCE_CONTENT; case Source.liveSamples: return SOURCE_LIVE_SAMPLES; - case Source.rumba: + case Source.api: return SOURCE_RUMBA; default: return ""; diff --git a/cloud-function/src/handlers/stripe-plans.ts b/cloud-function/src/handlers/handle-stripe-plans.ts similarity index 96% rename from cloud-function/src/handlers/stripe-plans.ts rename to cloud-function/src/handlers/handle-stripe-plans.ts index c9abfcb89eca..03eabafbef5e 100644 --- a/cloud-function/src/handlers/stripe-plans.ts +++ b/cloud-function/src/handlers/handle-stripe-plans.ts @@ -15,7 +15,10 @@ interface Result { plans: PlanResult; } -export async function stripePlans(req: express.Request, res: express.Response) { +export async function handleStripePlans( + req: express.Request, + res: express.Response +) { const lookupData = ORIGIN_MAIN === "developer.mozilla.org" ? prodLookup : stageLookup; diff --git a/cloud-function/src/handlers/rumba.ts b/cloud-function/src/handlers/proxy-api.ts similarity index 74% rename from cloud-function/src/handlers/rumba.ts rename to cloud-function/src/handlers/proxy-api.ts index 2b68ae098245..7fbfaeb067de 100644 --- a/cloud-function/src/handlers/rumba.ts +++ b/cloud-function/src/handlers/proxy-api.ts @@ -2,8 +2,8 @@ import { createProxyMiddleware, fixRequestBody } from "http-proxy-middleware"; import { Source, sourceUri } from "../env.js"; -export const proxyRumba = createProxyMiddleware({ - target: sourceUri(Source.rumba), +export const proxyApi = createProxyMiddleware({ + target: sourceUri(Source.api), changeOrigin: true, autoRewrite: true, proxyTimeout: 20000, diff --git a/cloud-function/src/handlers/content.ts b/cloud-function/src/handlers/proxy-content.ts similarity index 100% rename from cloud-function/src/handlers/content.ts rename to cloud-function/src/handlers/proxy-content.ts diff --git a/cloud-function/src/handlers/kevel.ts b/cloud-function/src/handlers/proxy-kevel.ts similarity index 100% rename from cloud-function/src/handlers/kevel.ts rename to cloud-function/src/handlers/proxy-kevel.ts diff --git a/cloud-function/src/handlers/telemetry.ts b/cloud-function/src/handlers/proxy-telemetry.ts similarity index 100% rename from cloud-function/src/handlers/telemetry.ts rename to cloud-function/src/handlers/proxy-telemetry.ts From e5c7b61d9f61770e81eb639b7e5c492e51c19094 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 21:52:14 +0200 Subject: [PATCH 303/343] refactor(cloud-function): return proxyContent handler from module --- cloud-function/src/app.ts | 4 +- cloud-function/src/handlers/proxy-content.ts | 50 ++++++++++---------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/cloud-function/src/app.ts b/cloud-function/src/app.ts index 2231e51fd74c..f2b57045fd8e 100644 --- a/cloud-function/src/app.ts +++ b/cloud-function/src/app.ts @@ -3,7 +3,7 @@ import { Router } from "express"; import compression from "compression"; import { Origin } from "./env.js"; -import { createContentProxy } from "./handlers/proxy-content.js"; +import { proxyContent } from "./handlers/proxy-content.js"; import { proxyKevel } from "./handlers/proxy-kevel.js"; import { proxyApi } from "./handlers/proxy-api.js"; import { handleStripePlans } from "./handlers/handle-stripe-plans.js"; @@ -18,8 +18,6 @@ import { redirectTrailingSlash } from "./middlewares/redirect-trailing-slash.js" import { requireOrigin } from "./middlewares/require-origin.js"; import { notFound } from "./middlewares/not-found.js"; -const proxyContent = createContentProxy(); - const router = Router(); router.use(redirectLeadingSlash); router.all( diff --git a/cloud-function/src/handlers/proxy-content.ts b/cloud-function/src/handlers/proxy-content.ts index 4f31280ac8d5..455e0c43ce26 100644 --- a/cloud-function/src/handlers/proxy-content.ts +++ b/cloud-function/src/handlers/proxy-content.ts @@ -1,4 +1,3 @@ -import type express from "express"; import { createProxyMiddleware, fixRequestBody, @@ -12,30 +11,29 @@ const NOT_FOUND_PATH = "en-us/_spas/404.html"; let notFoundBuffer: ArrayBuffer; -export function createContentProxy(): express.Handler { - const target = sourceUri(Source.content); - return createProxyMiddleware({ - target, - changeOrigin: true, - autoRewrite: true, - proxyTimeout: 20000, - xfwd: true, - selfHandleResponse: true, - onProxyReq: fixRequestBody, - onProxyRes: responseInterceptor( - async (responseBuffer, proxyRes, req, res) => { - withContentResponseHeaders(proxyRes, req, res); - if (proxyRes.statusCode === 404) { - if (!notFoundBuffer) { - const response = await fetch(`${target}${NOT_FOUND_PATH}`); - notFoundBuffer = await response.arrayBuffer(); - } - res.setHeader("Content-Type", "text/html"); - return Buffer.from(notFoundBuffer); - } +const target = sourceUri(Source.content); - return responseBuffer; +export const proxyContent = createProxyMiddleware({ + target, + changeOrigin: true, + autoRewrite: true, + proxyTimeout: 20000, + xfwd: true, + selfHandleResponse: true, + onProxyReq: fixRequestBody, + onProxyRes: responseInterceptor( + async (responseBuffer, proxyRes, req, res) => { + withContentResponseHeaders(proxyRes, req, res); + if (proxyRes.statusCode === 404) { + if (!notFoundBuffer) { + const response = await fetch(`${target}${NOT_FOUND_PATH}`); + notFoundBuffer = await response.arrayBuffer(); + } + res.setHeader("Content-Type", "text/html"); + return Buffer.from(notFoundBuffer); } - ), - }); -} + + return responseBuffer; + } + ), +}); From 7b50dbdc56af2459c11952f3b3cf69470c817a15 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 21:53:06 +0200 Subject: [PATCH 304/343] fixup! refactor(cloud-function): rename {plans => stripePlans} --- cloud-function/src/handlers/handle-stripe-plans.ts | 4 ++-- cloud-function/src/{plans => stripe-plans}/index.ts | 0 cloud-function/src/{plans => stripe-plans}/prod.ts | 0 cloud-function/src/{plans => stripe-plans}/stage.ts | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename cloud-function/src/{plans => stripe-plans}/index.ts (100%) rename cloud-function/src/{plans => stripe-plans}/prod.ts (100%) rename cloud-function/src/{plans => stripe-plans}/stage.ts (100%) diff --git a/cloud-function/src/handlers/handle-stripe-plans.ts b/cloud-function/src/handlers/handle-stripe-plans.ts index 03eabafbef5e..5915e1ad2b38 100644 --- a/cloud-function/src/handlers/handle-stripe-plans.ts +++ b/cloud-function/src/handlers/handle-stripe-plans.ts @@ -3,8 +3,8 @@ import type express from "express"; import acceptLanguageParser from "accept-language-parser"; import { ORIGIN_MAIN } from "../env.js"; import { getRequestCountry } from "../utils.js"; -import stageLookup from "../plans/stage.js"; -import prodLookup from "../plans/prod.js"; +import stageLookup from "../stripe-plans/stage.js"; +import prodLookup from "../stripe-plans/prod.js"; interface PlanResult { [name: string]: { monthlyPriceInCents: number; id: string }; diff --git a/cloud-function/src/plans/index.ts b/cloud-function/src/stripe-plans/index.ts similarity index 100% rename from cloud-function/src/plans/index.ts rename to cloud-function/src/stripe-plans/index.ts diff --git a/cloud-function/src/plans/prod.ts b/cloud-function/src/stripe-plans/prod.ts similarity index 100% rename from cloud-function/src/plans/prod.ts rename to cloud-function/src/stripe-plans/prod.ts diff --git a/cloud-function/src/plans/stage.ts b/cloud-function/src/stripe-plans/stage.ts similarity index 100% rename from cloud-function/src/plans/stage.ts rename to cloud-function/src/stripe-plans/stage.ts From 580328fbb79ad847f8f311713798240d2807977a Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 21:54:03 +0200 Subject: [PATCH 305/343] fixup! refactor(gcp/function): move {gcp/function => cloud-function} --- cloud-function/src/build-redirects.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud-function/src/build-redirects.ts b/cloud-function/src/build-redirects.ts index 5135513d4576..f340d78b1307 100644 --- a/cloud-function/src/build-redirects.ts +++ b/cloud-function/src/build-redirects.ts @@ -8,7 +8,7 @@ import { VALID_LOCALES } from "./internal/constants/index.js"; const dirname = path.dirname(fileURLToPath(import.meta.url)); -const root = path.join(dirname, "..", "..", ".."); +const root = path.join(dirname, "..", ".."); dotenv.config({ path: path.join(root, process.env["ENV_FILE"] || ".env"), }); From f00e08f76df709276ef9dfccc36126d4eee7f08c Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 21:55:41 +0200 Subject: [PATCH 306/343] refactor(cloud-function): extract PROXY_TIMEOUT --- cloud-function/src/constants.ts | 2 ++ cloud-function/src/handlers/proxy-api.ts | 3 ++- cloud-function/src/handlers/proxy-content.ts | 3 ++- cloud-function/src/handlers/proxy-telemetry.ts | 3 ++- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/cloud-function/src/constants.ts b/cloud-function/src/constants.ts index ecff4f27ffbc..11579b469e4f 100644 --- a/cloud-function/src/constants.ts +++ b/cloud-function/src/constants.ts @@ -1,3 +1,5 @@ export const DEFAULT_COUNTRY = "US"; +export const PROXY_TIMEOUT = 20000; + export const THIRTY_DAYS = 3600 * 24 * 30; diff --git a/cloud-function/src/handlers/proxy-api.ts b/cloud-function/src/handlers/proxy-api.ts index 7fbfaeb067de..24c03b0da2e6 100644 --- a/cloud-function/src/handlers/proxy-api.ts +++ b/cloud-function/src/handlers/proxy-api.ts @@ -1,12 +1,13 @@ import { createProxyMiddleware, fixRequestBody } from "http-proxy-middleware"; import { Source, sourceUri } from "../env.js"; +import { PROXY_TIMEOUT } from "../constants.js"; export const proxyApi = createProxyMiddleware({ target: sourceUri(Source.api), changeOrigin: true, autoRewrite: true, - proxyTimeout: 20000, + proxyTimeout: PROXY_TIMEOUT, xfwd: true, onProxyReq: fixRequestBody, }); diff --git a/cloud-function/src/handlers/proxy-content.ts b/cloud-function/src/handlers/proxy-content.ts index 455e0c43ce26..d7941b399b85 100644 --- a/cloud-function/src/handlers/proxy-content.ts +++ b/cloud-function/src/handlers/proxy-content.ts @@ -6,6 +6,7 @@ import { import { withContentResponseHeaders } from "../headers.js"; import { Source, sourceUri } from "../env.js"; +import { PROXY_TIMEOUT } from "../constants.js"; const NOT_FOUND_PATH = "en-us/_spas/404.html"; @@ -17,7 +18,7 @@ export const proxyContent = createProxyMiddleware({ target, changeOrigin: true, autoRewrite: true, - proxyTimeout: 20000, + proxyTimeout: PROXY_TIMEOUT, xfwd: true, selfHandleResponse: true, onProxyReq: fixRequestBody, diff --git a/cloud-function/src/handlers/proxy-telemetry.ts b/cloud-function/src/handlers/proxy-telemetry.ts index 940206518f7a..65194b130e41 100644 --- a/cloud-function/src/handlers/proxy-telemetry.ts +++ b/cloud-function/src/handlers/proxy-telemetry.ts @@ -1,10 +1,11 @@ import { createProxyMiddleware, fixRequestBody } from "http-proxy-middleware"; +import { PROXY_TIMEOUT } from "../constants.js"; export const proxyTelemetry = createProxyMiddleware({ target: "https://incoming.telemetry.mozilla.org", changeOrigin: true, autoRewrite: true, - proxyTimeout: 20000, + proxyTimeout: PROXY_TIMEOUT, xfwd: true, onProxyReq: fixRequestBody, }); From d60e40d99596d295c69c7f70c7a2fb14bd9f358f Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 21:58:39 +0200 Subject: [PATCH 307/343] ci(xyz-build): add workflow_dispatch input --- .github/workflows/xyz-build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 6288d4fb03e4..ebf345bde19e 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -9,6 +9,11 @@ on: - cloud-function/** workflow_dispatch: + inputs: + notes: + description: "Notes" + required: false + default: ${DEFAULT_NOTES} workflow_call: secrets: From 524394f58db3eed4f4c7294e693bb6a1270e2ecd Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 21:59:19 +0200 Subject: [PATCH 308/343] ci(xyz-build): build on push to gcp --- .github/workflows/xyz-build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index ebf345bde19e..1972406f9796 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - gcp paths: - .github/workflows/xyz-build.yml - cloud-function/** @@ -13,7 +14,7 @@ on: notes: description: "Notes" required: false - default: ${DEFAULT_NOTES} + default: "" workflow_call: secrets: From 0297a55fb5594ebcb19d43d453b547cf7136f8f4 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 22:24:24 +0200 Subject: [PATCH 309/343] docs(cloud-function): format README --- cloud-function/README.md | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/cloud-function/README.md b/cloud-function/README.md index 97fc573bef6a..ce7abbd52d3c 100644 --- a/cloud-function/README.md +++ b/cloud-function/README.md @@ -1,22 +1,31 @@ # Cloud Function -This is MDN's HTTP request handler, deployed using [Cloud Functions](https://cloud.google.com/functions/) behind [Cloud CDN](https://cloud.google.com/cdn/). It mostly proxies requests and handles some special routes directly. +This is MDN's HTTP request handler, deployed using +[Cloud Functions](https://cloud.google.com/functions/) behind +[Cloud CDN](https://cloud.google.com/cdn/). It mostly proxies requests and +handles some special routes directly. ## Environment variables The function uses the following environment variables: -* `ORIGIN_MAIN` (default: `"localhost"`) - The expected `Host` header value for requests to the main site. -* `ORIGIN_LIVE_SAMPLES` (default: `"localhost"`) - The expected `Host` header value for requests to live samples. -* `SOURCE_CONTENT` (default: `"http://localhost:8100"`) - The URL at which the built content is served. -* `SOURCE_RUMBA` (default: `"http://localhost:8000"`) - The URL at which the API is served. +- `ORIGIN_MAIN` (default: `"localhost"`) - The expected `Host` header value for + requests to the main site. +- `ORIGIN_LIVE_SAMPLES` (default: `"localhost"`) - The expected `Host` header + value for requests to live samples. +- `SOURCE_CONTENT` (default: `"http://localhost:8100"`) - The URL at which the + built content is served. +- `SOURCE_RUMBA` (default: `"http://localhost:8000"`) - The URL at which the API + is served. The placement handler uses the following environment variables: -* `KEVEL_SITE_ID` (default: `0`) - Required for serving placements via Kevel. -* `KEVEL_NETWORK_ID` (default: `0`) - Required for serving placements via Kevel. -* `SIGN_SECRET` (default: `""`) - Required for serving placements. -* `CARBON_ZONE_KEY` (default: `""`) - Required for serving placements via Carbon. -* `CARBON_FALLBACK_ENABLED` (default: `"false"`) - Whether fallback placements should be served via Carbon. +- `KEVEL_SITE_ID` (default: `0`) - Required for serving placements via Kevel. +- `KEVEL_NETWORK_ID` (default: `0`) - Required for serving placements via Kevel. +- `SIGN_SECRET` (default: `""`) - Required for serving placements. +- `CARBON_ZONE_KEY` (default: `""`) - Required for serving placements via + Carbon. +- `CARBON_FALLBACK_ENABLED` (default: `"false"`) - Whether fallback placements + should be served via Carbon. You can override the defaults by adding `.env` file with `KEY=value` lines. From af0423d491c01d25714ce92013dcfbbb8e9cf7ee Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 22:25:48 +0200 Subject: [PATCH 310/343] style(prettier): ignore compiled JS in cloud-function --- .prettierignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.prettierignore b/.prettierignore index 6fca7089be7a..20b223b967c3 100644 --- a/.prettierignore +++ b/.prettierignore @@ -30,3 +30,4 @@ _githistory.json popularities.json /client/public/service-worker.js /cloud-function/src/internal/ +/cloud-function/**/*.js From 5092ee3d28bfa490422ef8d6886cc5e5ffa8a1d8 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 22:27:49 +0200 Subject: [PATCH 311/343] style(eslint): ignore compiled JS in cloud-function --- .eslintignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintignore b/.eslintignore index 1c755258f465..59c83e97f089 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,6 +7,7 @@ client/public/service-worker.js client/public/ client/src/document/*.js cloud-function/src/internal/ +cloud-function/**/*.js filecheck/*.js mdn/content/ tool/*.js From 38690977d00bcd800f5018a648f3881be456cef2 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 22:28:59 +0200 Subject: [PATCH 312/343] style: add trailing line --- cloud-function/Procfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud-function/Procfile b/cloud-function/Procfile index f8544f00c5ca..898c10d52fbb 100644 --- a/cloud-function/Procfile +++ b/cloud-function/Procfile @@ -1,2 +1,2 @@ proxy: npm run proxy -server: npm run server:watch \ No newline at end of file +server: npm run server:watch From b8dfbbb58d1caddcf57341e317d801bf4d5d59ee Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 22:44:00 +0200 Subject: [PATCH 313/343] refactor(cloud-function): rename LOCAL_{BUILD => CONTENT} --- cloud-function/src/env.ts | 6 +++--- cloud-function/src/proxy.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cloud-function/src/env.ts b/cloud-function/src/env.ts index aee55a52bd01..154cb4756cfd 100644 --- a/cloud-function/src/env.ts +++ b/cloud-function/src/env.ts @@ -7,7 +7,7 @@ dotenv.config({ path: path.join(cwd(), process.env["ENV_FILE"] || ".env"), }); -export const LOCAL_BUILD = "http://localhost:8100/"; +export const LOCAL_CONTENT = "http://localhost:8100/"; export const LOCAL_RUMBA = "http://localhost:8000/"; export enum Origin { @@ -38,9 +38,9 @@ export function origin(req: express.Request): Origin { } export const SOURCE_CONTENT: string = - process.env["SOURCE_CONTENT"] || LOCAL_BUILD; + process.env["SOURCE_CONTENT"] || LOCAL_CONTENT; export const SOURCE_LIVE_SAMPLES: string = - process.env["SOURCE_LIVE_SAMPLES"] || LOCAL_BUILD; + process.env["SOURCE_LIVE_SAMPLES"] || LOCAL_CONTENT; export const SOURCE_RUMBA: string = process.env["SOURCE_RUMBA"] || "http://localhost:8000/"; diff --git a/cloud-function/src/proxy.ts b/cloud-function/src/proxy.ts index 53951cb363be..5a44fae52976 100644 --- a/cloud-function/src/proxy.ts +++ b/cloud-function/src/proxy.ts @@ -6,13 +6,13 @@ import httpServer from "http-server"; import { HTTPS_CERT_FILE, HTTPS_KEY_FILE, - LOCAL_BUILD, + LOCAL_CONTENT, SOURCE_CONTENT, SOURCE_LIVE_SAMPLES, } from "./env.js"; -if ([SOURCE_CONTENT, SOURCE_LIVE_SAMPLES].includes(LOCAL_BUILD)) { - const url = new URL(LOCAL_BUILD); +if ([SOURCE_CONTENT, SOURCE_LIVE_SAMPLES].includes(LOCAL_CONTENT)) { + const url = new URL(LOCAL_CONTENT); const contentServer = httpServer.createServer({ root: "../client/build", }); From 8fe852d0a1c5690bd5cfc572bc4a84a64217c2d0 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 22:47:43 +0200 Subject: [PATCH 314/343] refactor(cloud-function): remove SOURCE_LIVE_SAMPLES --- cloud-function/src/env.ts | 4 ---- cloud-function/src/proxy.ts | 3 +-- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/cloud-function/src/env.ts b/cloud-function/src/env.ts index 154cb4756cfd..0e407475afef 100644 --- a/cloud-function/src/env.ts +++ b/cloud-function/src/env.ts @@ -39,8 +39,6 @@ export function origin(req: express.Request): Origin { export const SOURCE_CONTENT: string = process.env["SOURCE_CONTENT"] || LOCAL_CONTENT; -export const SOURCE_LIVE_SAMPLES: string = - process.env["SOURCE_LIVE_SAMPLES"] || LOCAL_CONTENT; export const SOURCE_RUMBA: string = process.env["SOURCE_RUMBA"] || "http://localhost:8000/"; @@ -58,8 +56,6 @@ export function sourceUri(source: Source): string { switch (source) { case Source.content: return SOURCE_CONTENT; - case Source.liveSamples: - return SOURCE_LIVE_SAMPLES; case Source.api: return SOURCE_RUMBA; default: diff --git a/cloud-function/src/proxy.ts b/cloud-function/src/proxy.ts index 5a44fae52976..69fa27ecfa05 100644 --- a/cloud-function/src/proxy.ts +++ b/cloud-function/src/proxy.ts @@ -8,10 +8,9 @@ import { HTTPS_KEY_FILE, LOCAL_CONTENT, SOURCE_CONTENT, - SOURCE_LIVE_SAMPLES, } from "./env.js"; -if ([SOURCE_CONTENT, SOURCE_LIVE_SAMPLES].includes(LOCAL_CONTENT)) { +if (SOURCE_CONTENT === LOCAL_CONTENT) { const url = new URL(LOCAL_CONTENT); const contentServer = httpServer.createServer({ root: "../client/build", From fd1a554b7b47002458ad2bf75b26377ed5f713e7 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 22:52:46 +0200 Subject: [PATCH 315/343] refactor(cloud-function): rename SOURCE_{RUMBA => API} --- .github/workflows/prod-build.yml | 2 +- .github/workflows/stage-build.yml | 2 +- .github/workflows/xyz-build.yml | 2 +- cloud-function/README.md | 2 +- cloud-function/src/env.ts | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/prod-build.yml b/.github/workflows/prod-build.yml index 5841d871c2ba..27dbb80b831c 100644 --- a/.github/workflows/prod-build.yml +++ b/.github/workflows/prod-build.yml @@ -346,7 +346,7 @@ jobs: --set-env-vars="ORIGIN_MAIN=developer.mozilla.org" \ --set-env-vars="ORIGIN_LIVE_SAMPLES=live-samples.mdn.mozilla.net" \ --set-env-vars="SOURCE_CONTENT=https://storage.googleapis.com/${{ vars.GCP_BUCKET_NAME }}/main/" \ - --set-env-vars="SOURCE_RUMBA=https://api.developer.mozilla.org/" \ + --set-env-vars="SOURCE_API=https://api.developer.mozilla.org/" \ --set-env-vars="SENTRY_DSN=${{ secrets.SENTRY_DSN_CLOUD_FUNCTION }}" \ --set-env-vars="SENTRY_ENVIRONMENT=prod" \ --set-env-vars="SENTRY_TRACES_SAMPLE_RATE=${{ vars.SENTRY_TRACES_SAMPLE_RATE }}" \ diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 78d9960eb52e..8698d97d48d2 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -344,7 +344,7 @@ jobs: --set-env-vars="ORIGIN_MAIN=developer.allizom.org" \ --set-env-vars="ORIGIN_LIVE_SAMPLES=live-samples.mdn.allizom.net" \ --set-env-vars="SOURCE_CONTENT=https://storage.googleapis.com/${{ vars.GCP_BUCKET_NAME }}/main/" \ - --set-env-vars="SOURCE_RUMBA=https://api.developer.allizom.org/" \ + --set-env-vars="SOURCE_API=https://api.developer.allizom.org/" \ --set-env-vars="SENTRY_DSN=${{ secrets.SENTRY_DSN_CLOUD_FUNCTION }}" \ --set-env-vars="SENTRY_ENVIRONMENT=stage" \ --set-env-vars="SENTRY_TRACES_SAMPLE_RATE=${{ vars.SENTRY_TRACES_SAMPLE_RATE }}" \ diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 1972406f9796..f432f1b5f9da 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -217,7 +217,7 @@ jobs: --set-env-vars="ORIGIN_MAIN=${{ vars.ORIGIN_MAIN }}" \ --set-env-vars="ORIGIN_LIVE_SAMPLES=${{ vars.ORIGIN_LIVE_SAMPLES }}" \ --set-env-vars="SOURCE_CONTENT=https://storage.googleapis.com/${{ vars.GCP_BUCKET_NAME }}/${{ env.BUCKET_PATH }}/" \ - --set-env-vars="SOURCE_RUMBA=https://api.developer.allizom.org/" \ + --set-env-vars="SOURCE_API=https://api.developer.allizom.org/" \ --set-env-vars="SENTRY_DSN=${{ secrets.SENTRY_DSN_CLOUD_FUNCTION }}" \ --set-env-vars="SENTRY_ENVIRONMENT=xyz" \ --set-env-vars="SENTRY_TRACES_SAMPLE_RATE=${{ vars.SENTRY_TRACES_SAMPLE_RATE }}" \ diff --git a/cloud-function/README.md b/cloud-function/README.md index ce7abbd52d3c..9c8e8f30fada 100644 --- a/cloud-function/README.md +++ b/cloud-function/README.md @@ -15,7 +15,7 @@ The function uses the following environment variables: value for requests to live samples. - `SOURCE_CONTENT` (default: `"http://localhost:8100"`) - The URL at which the built content is served. -- `SOURCE_RUMBA` (default: `"http://localhost:8000"`) - The URL at which the API +- `SOURCE_API` (default: `"http://localhost:8000"`) - The URL at which the API is served. The placement handler uses the following environment variables: diff --git a/cloud-function/src/env.ts b/cloud-function/src/env.ts index 0e407475afef..72c100181d2a 100644 --- a/cloud-function/src/env.ts +++ b/cloud-function/src/env.ts @@ -39,8 +39,8 @@ export function origin(req: express.Request): Origin { export const SOURCE_CONTENT: string = process.env["SOURCE_CONTENT"] || LOCAL_CONTENT; -export const SOURCE_RUMBA: string = - process.env["SOURCE_RUMBA"] || "http://localhost:8000/"; +export const SOURCE_API: string = + process.env["SOURCE_API"] || "http://localhost:8000/"; export function getOriginFromRequest(req: express.Request): Origin { if (req.hostname === ORIGIN_MAIN && !req.path.includes("/_sample_.")) { @@ -57,7 +57,7 @@ export function sourceUri(source: Source): string { case Source.content: return SOURCE_CONTENT; case Source.api: - return SOURCE_RUMBA; + return SOURCE_API; default: return ""; } From ffb200be9a6619df1e224a1debd732ea33f67f8b Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 22:57:37 +0200 Subject: [PATCH 316/343] ci(xyz-build): inline ORIGIN_{MAIN,LIVE_SAMPLES} --- .github/workflows/xyz-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index f432f1b5f9da..0ed3cd181c87 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -214,8 +214,8 @@ jobs: --concurrency=100 \ --memory=2GB \ --timeout=30s \ - --set-env-vars="ORIGIN_MAIN=${{ vars.ORIGIN_MAIN }}" \ - --set-env-vars="ORIGIN_LIVE_SAMPLES=${{ vars.ORIGIN_LIVE_SAMPLES }}" \ + --set-env-vars="ORIGIN_MAIN=developer.allizom.xyz" \ + --set-env-vars="ORIGIN_LIVE_SAMPLES=live-samples.developer.allizom.xyz" \ --set-env-vars="SOURCE_CONTENT=https://storage.googleapis.com/${{ vars.GCP_BUCKET_NAME }}/${{ env.BUCKET_PATH }}/" \ --set-env-vars="SOURCE_API=https://api.developer.allizom.org/" \ --set-env-vars="SENTRY_DSN=${{ secrets.SENTRY_DSN_CLOUD_FUNCTION }}" \ From b6f78b63dc32c7fdeefc854b053e473cb65e47db Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 22:59:29 +0200 Subject: [PATCH 317/343] ci(xyz-build): make GCP_LOAD_BALANCER_NAME a secret --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 0ed3cd181c87..ddecaa334644 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -254,4 +254,4 @@ jobs: - name: Invalidate CDN run: |- - gcloud compute url-maps invalidate-cdn-cache ${{ vars.GCP_LOAD_BALANCER_NAME }} --path "/*" + gcloud compute url-maps invalidate-cdn-cache ${{ secrets.GCP_LOAD_BALANCER_NAME }} --path "/*" From 47145ffbed02088e2e3e869a27fc3ba8447a7db3 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 23:12:11 +0200 Subject: [PATCH 318/343] ci(xyz-build): shorten name --- .github/workflows/xyz-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index ddecaa334644..5ae9c001f569 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -1,4 +1,4 @@ -name: XYZ Build (GCP) +name: XYZ Build on: push: From 0ed527a360197e8e9dcd4026bc5887e7049267c5 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 23:20:26 +0200 Subject: [PATCH 319/343] ci(xyz-build): generate whatsdeployed --- .github/workflows/xyz-build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml index 5ae9c001f569..4024fb6784fb 100644 --- a/.github/workflows/xyz-build.yml +++ b/.github/workflows/xyz-build.yml @@ -169,6 +169,11 @@ jobs: # Generate sitemap index file yarn build --sitemap-index + # Generate whatsdeployed files. + yarn tool whatsdeployed --output client/build/_whatsdeployed/code.json + yarn tool whatsdeployed $CONTENT_ROOT --output client/build/_whatsdeployed/content.json + yarn tool whatsdeployed $CONTENT_TRANSLATED_ROOT --output client/build/_whatsdeployed/translated-content.json + - name: Authenticate with GCP uses: google-github-actions/auth@v0 with: From 57fed8bbe6e7106e2b4023d45e085d87c2dabd30 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 23:22:00 +0200 Subject: [PATCH 320/343] chore(cloud-function): add .env-dist --- cloud-function/.env-dist | 5 +++++ cloud-function/.gitignore | 1 + 2 files changed, 6 insertions(+) create mode 100644 cloud-function/.env-dist diff --git a/cloud-function/.env-dist b/cloud-function/.env-dist new file mode 100644 index 000000000000..c54cd9c8a7fe --- /dev/null +++ b/cloud-function/.env-dist @@ -0,0 +1,5 @@ +ORIGIN_MAIN="localhost" +ORIGIN_LIVE_SAMPLES="localhost" + +SOURCE_CONTENT=http://localhost:7000/ +SOURCE_RUMBA=http://localhost:8000/ diff --git a/cloud-function/.gitignore b/cloud-function/.gitignore index 9f83d01e07ab..deb96650c7f6 100644 --- a/cloud-function/.gitignore +++ b/cloud-function/.gitignore @@ -1,5 +1,6 @@ node_modules .env* +!.env-dist *.log redirects.json src/**/*.js From 676243c72441b767fc98bef87b7a7fc6fab1c0ae Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 23:48:22 +0200 Subject: [PATCH 321/343] chore(cloud-function): default to stage API for convenience --- cloud-function/README.md | 6 +++--- cloud-function/src/env.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cloud-function/README.md b/cloud-function/README.md index 9c8e8f30fada..3a5d82cca506 100644 --- a/cloud-function/README.md +++ b/cloud-function/README.md @@ -15,8 +15,8 @@ The function uses the following environment variables: value for requests to live samples. - `SOURCE_CONTENT` (default: `"http://localhost:8100"`) - The URL at which the built content is served. -- `SOURCE_API` (default: `"http://localhost:8000"`) - The URL at which the API - is served. +- `SOURCE_API` (default: `"https://developer.allizom.org/"`) - The URL at which + the API is served. The placement handler uses the following environment variables: @@ -28,4 +28,4 @@ The placement handler uses the following environment variables: - `CARBON_FALLBACK_ENABLED` (default: `"false"`) - Whether fallback placements should be served via Carbon. -You can override the defaults by adding `.env` file with `KEY=value` lines. +You can override the defaults by adding a `.env` file with `KEY=value` lines. diff --git a/cloud-function/src/env.ts b/cloud-function/src/env.ts index 72c100181d2a..2ea9cd53d2c5 100644 --- a/cloud-function/src/env.ts +++ b/cloud-function/src/env.ts @@ -40,7 +40,7 @@ export function origin(req: express.Request): Origin { export const SOURCE_CONTENT: string = process.env["SOURCE_CONTENT"] || LOCAL_CONTENT; export const SOURCE_API: string = - process.env["SOURCE_API"] || "http://localhost:8000/"; + process.env["SOURCE_API"] || "https://developer.allizom.org/"; export function getOriginFromRequest(req: express.Request): Origin { if (req.hostname === ORIGIN_MAIN && !req.path.includes("/_sample_.")) { From d20a923ed7f02a629ea39fc953757bf4c24d7482 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 23:57:20 +0200 Subject: [PATCH 322/343] docs(cloud-function): add quickstart to README --- cloud-function/README.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/cloud-function/README.md b/cloud-function/README.md index 3a5d82cca506..479fbb1629a4 100644 --- a/cloud-function/README.md +++ b/cloud-function/README.md @@ -5,6 +5,29 @@ This is MDN's HTTP request handler, deployed using [Cloud CDN](https://cloud.google.com/cdn/). It mostly proxies requests and handles some special routes directly. +## Quickstart + +Run `npm start` to serve the Cloud Function at http://localhost:7100/. + +By default, it will use your local `client/build` directory, serving it at +http://localhost:8100/, and proxy API requests to the stage API at +`https://developer.allizom.org/`. + +### How to use a local Rumba? + +Set `SOURCE_API=http://localhost:8000/` in your `.env.` + +### How to use Glean? + +To use Glean, the Cloud Function must be accessed via HTTPS. Otherwise the +Glean.js SDK throws an uncaught error that prevents execution of JavaScript. + +We recommend using [mkcert](https://github.com/FiloSottile/mkcert) to create a +locally-trusted development certificate. Add the key and certificate paths as +`HTTPS_KEY_FILE` and `HTTPS_CERT_FILE` variables to your `.env` file. This will +automatically enable an HTTPS proxy at https://localhost/ in addition to +`http://localhost:7100/`. + ## Environment variables The function uses the following environment variables: @@ -14,7 +37,7 @@ The function uses the following environment variables: - `ORIGIN_LIVE_SAMPLES` (default: `"localhost"`) - The expected `Host` header value for requests to live samples. - `SOURCE_CONTENT` (default: `"http://localhost:8100"`) - The URL at which the - built content is served. + client build is served. - `SOURCE_API` (default: `"https://developer.allizom.org/"`) - The URL at which the API is served. From 530221ca23b52631d45821be089e7ca04b745bef Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 17 Apr 2023 23:59:36 +0200 Subject: [PATCH 323/343] ci: remove xyz-build Extract to separate PR. --- .github/workflows/xyz-build.yml | 262 -------------------------------- 1 file changed, 262 deletions(-) delete mode 100644 .github/workflows/xyz-build.yml diff --git a/.github/workflows/xyz-build.yml b/.github/workflows/xyz-build.yml deleted file mode 100644 index 4024fb6784fb..000000000000 --- a/.github/workflows/xyz-build.yml +++ /dev/null @@ -1,262 +0,0 @@ -name: XYZ Build - -on: - push: - branches: - - main - - gcp - paths: - - .github/workflows/xyz-build.yml - - cloud-function/** - - workflow_dispatch: - inputs: - notes: - description: "Notes" - required: false - default: "" - - workflow_call: - secrets: - GCP_PROJECT_NAME: - required: true - GCS_BUCKET: - required: true - WIP_PROJECT_ID: - required: true - -permissions: - contents: read - id-token: write - -jobs: - build: - environment: xyz - runs-on: ubuntu-latest-4core - - env: - BUCKET_PATH: main - - # Only run the scheduled workflows on the main repo. - if: github.repository == 'mdn/yari' - - steps: - - uses: actions/checkout@v3 - - - uses: actions/checkout@v3 - with: - repository: mdn/content - path: mdn/content - # Yes, this means fetch EVERY COMMIT EVER. - # It's probably not sustainable in the far future (e.g. past 2021) - # but for now it's good enough. We'll need all the history - # so we can figure out each document's last-modified date. - fetch-depth: 0 - - - uses: actions/checkout@v3 - with: - repository: mdn/translated-content - path: mdn/translated-content - # See matching warning for mdn/content checkout step - fetch-depth: 0 - - - uses: actions/checkout@v3 - with: - repository: mdn/mdn-contributor-spotlight - path: mdn/mdn-contributor-spotlight - - - name: Setup Node.js environment - uses: actions/setup-node@v3 - with: - node-version: 18 - cache: yarn - - - name: Install all yarn packages - if: ${{ ! vars.SKIP_BUILD }} - run: yarn --frozen-lockfile - - - name: Print information about CPU - run: cat /proc/cpuinfo - - - name: Build everything - if: ${{ ! vars.SKIP_BUILD }} - env: - # Remember, the mdn/content repo got cloned into `pwd` into a - # sub-folder called "mdn/content" - CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files - CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files - CONTRIBUTOR_SPOTLIGHT_ROOT: ${{ github.workspace }}/mdn/mdn-contributor-spotlight/contributors - - # The default for this environment variable is geared for writers - # (aka. local development). Usually defaults are supposed to be for - # secure production but this is an exception and default - # is not insecure. - BUILD_LIVE_SAMPLES_BASE_URL: https://live-samples.developer.allizom.xyz - - # Use the stage version of interactive examples. - BUILD_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net - - # Now is not the time to worry about flaws. - BUILD_FLAW_LEVELS: "*:ignore" - - # This is the Google Analytics account ID for developer.mozilla.org - # If it's used on other domains (e.g. stage or dev builds), it's OK - # because ultimately Google Analytics will filter it out since the - # origin domain isn't what that account expects. - #BUILD_GOOGLE_ANALYTICS_ACCOUNT: UA-36116321-5 - - # This enables the Plus call-to-action banner and the Plus landing page - REACT_APP_ENABLE_PLUS: true - - # This adds the ability to sign in (stage only for now) - REACT_APP_DISABLE_AUTH: false - - # Use the stage version of interactive examples in react app - REACT_APP_INTERACTIVE_EXAMPLES_BASE_URL: https://interactive-examples.mdn.allizom.net - - # Firefox Accounts and SubPlat settings - REACT_APP_FXA_SIGNIN_URL: /users/fxa/login/authenticate/ - REACT_APP_FXA_SETTINGS_URL: https://accounts.stage.mozaws.net/settings/ - REACT_APP_MDN_PLUS_SUBSCRIBE_URL: https://accounts.stage.mozaws.net/subscriptions/products/prod_Jtbg9tyGyLRuB0 - REACT_APP_MDN_PLUS_5M_PLAN: price_1JFoTYKb9q6OnNsLalexa03p - REACT_APP_MDN_PLUS_5Y_PLAN: price_1JpIPwKb9q6OnNsLJLsIqMp7 - REACT_APP_MDN_PLUS_10M_PLAN: price_1K6X7gKb9q6OnNsLi44HdLcC - REACT_APP_MDN_PLUS_10Y_PLAN: price_1K6X8VKb9q6OnNsLFlUcEiu4 - - # Surveys. - REACT_APP_SURVEY_START_CONTENT_DISCOVERY_2023: 0 # stage - REACT_APP_SURVEY_END_CONTENT_DISCOVERY_2023: 1677672000000 # (new Date("2023-03-01 12:00:00Z")).getTime() - REACT_APP_SURVEY_RATE_FROM_CONTENT_DISCOVERY_2023: 0.0 - REACT_APP_SURVEY_RATE_TILL_CONTENT_DISCOVERY_2023: 0.05 # 5% - - # Telemetry. - REACT_APP_GLEAN_CHANNEL: xyz - REACT_APP_GLEAN_ENABLED: true - - # Newsletter - REACT_APP_NEWSLETTER_ENABLED: false - - # Placement - REACT_APP_PLACEMENT_ENABLED: true - - run: | - - # Info about which CONTENT_* environment variables were set and to what. - echo "CONTENT_ROOT=$CONTENT_ROOT" - echo "CONTENT_TRANSLATED_ROOT=$CONTENT_TRANSLATED_ROOT" - # Build the ServiceWorker first - yarn build:sw - yarn build:prepare - - # (July 15, 2021) This is a temporary solution. This should become an - # integrated part of 'build:prepare'. - # See https://github.com/mdn/yari/issues/4217 - yarn tool popularities - - yarn tool sync-translated-content - - for locale in en-us es fr ja ko pt-br ru zh-cn zh-tw; do - yarn build --locale $locale 2>&1 | sed "s/^/[$locale] /" & - pids+=($!) - done - - for pid in "${pids[@]}"; do - wait $pid - done - - du -sh client/build - - # Generate sitemap index file - yarn build --sitemap-index - - # Generate whatsdeployed files. - yarn tool whatsdeployed --output client/build/_whatsdeployed/code.json - yarn tool whatsdeployed $CONTENT_ROOT --output client/build/_whatsdeployed/content.json - yarn tool whatsdeployed $CONTENT_TRANSLATED_ROOT --output client/build/_whatsdeployed/translated-content.json - - - name: Authenticate with GCP - uses: google-github-actions/auth@v0 - with: - token_format: access_token - service_account: deploy-xyz-yari@${{ secrets.GCP_PROJECT_NAME }}.iam.gserviceaccount.com - workload_identity_provider: projects/${{ secrets.WIP_PROJECT_ID }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions - - - name: Setup gcloud - uses: google-github-actions/setup-gcloud@v1 - with: - install_components: "beta" - - - name: Sync build with GCS bucket - if: ${{ ! vars.SKIP_BUILD }} - run: | - gsutil -q -m cp -r client/build/static gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH/static - gsutil -q -m rsync -cdrj html,json,txt client/build gs://${{ vars.GCP_BUCKET_NAME }}/$BUCKET_PATH - - - name: Generate redirects map - if: ${{ ! vars.SKIP_FUNCTION }} - working-directory: cloud-function - env: - CONTENT_ROOT: ${{ github.workspace }}/mdn/content/files - CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files - run: | - npm ci - npm run build-redirects - - - name: Deploy Function - if: ${{ ! vars.SKIP_FUNCTION }} - run: |- - for region in europe-west1 us-west1 asia-east1; do - gcloud beta functions deploy mdn-xyz-$region \ - --gen2 \ - --runtime=nodejs18 \ - --region=$region \ - --source=cloud-function \ - --trigger-http \ - --allow-unauthenticated \ - --entry-point=mdnHandler \ - --min-instances=1 \ - --max-instances=100 \ - --concurrency=100 \ - --memory=2GB \ - --timeout=30s \ - --set-env-vars="ORIGIN_MAIN=developer.allizom.xyz" \ - --set-env-vars="ORIGIN_LIVE_SAMPLES=live-samples.developer.allizom.xyz" \ - --set-env-vars="SOURCE_CONTENT=https://storage.googleapis.com/${{ vars.GCP_BUCKET_NAME }}/${{ env.BUCKET_PATH }}/" \ - --set-env-vars="SOURCE_API=https://api.developer.allizom.org/" \ - --set-env-vars="SENTRY_DSN=${{ secrets.SENTRY_DSN_CLOUD_FUNCTION }}" \ - --set-env-vars="SENTRY_ENVIRONMENT=xyz" \ - --set-env-vars="SENTRY_TRACES_SAMPLE_RATE=${{ vars.SENTRY_TRACES_SAMPLE_RATE }}" \ - --set-env-vars="SENTRY_RELEASE=${{ github.sha }}" \ - --set-secrets="KEVEL_SITE_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-site-id/versions/latest" \ - --set-secrets="KEVEL_NETWORK_ID=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-kevel-network-id/versions/latest" \ - --set-secrets="SIGN_SECRET=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-sign-secret/versions/latest" \ - --set-secrets="CARBON_ZONE_KEY=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-carbon-zone-key/versions/latest" \ - --set-secrets="CARBON_FALLBACK_ENABLED=projects/${{ secrets.WIP_PROJECT_ID }}/secrets/stage-fallback-enabled/versions/latest" \ - 2>&1 | sed "s/^/[$region] /" & - pids+=($!) - done - - for pid in "${pids[@]}"; do - wait $pid - done - - invalidate: - environment: xyz - needs: build - if: ${{ ! vars.SKIP_INVALIDATE }} - runs-on: ubuntu-latest - - steps: - - name: Authenticate with GCP - uses: google-github-actions/auth@v0 - with: - token_format: access_token - service_account: deploy-xyz-yari@${{ secrets.GCP_PROJECT_NAME }}.iam.gserviceaccount.com - workload_identity_provider: projects/${{ secrets.WIP_PROJECT_ID }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions - - - name: Setup gcloud - uses: google-github-actions/setup-gcloud@v1 - - - name: Invalidate CDN - run: |- - gcloud compute url-maps invalidate-cdn-cache ${{ secrets.GCP_LOAD_BALANCER_NAME }} --path "/*" From 5ab46930ab3b4f770ae165eb9fbf791c37c47708 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 18 Apr 2023 00:00:37 +0200 Subject: [PATCH 324/343] fixup! chore(cloud-function): add .env-dist --- cloud-function/.env-dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud-function/.env-dist b/cloud-function/.env-dist index c54cd9c8a7fe..79b10c57dd9b 100644 --- a/cloud-function/.env-dist +++ b/cloud-function/.env-dist @@ -1,5 +1,5 @@ ORIGIN_MAIN="localhost" ORIGIN_LIVE_SAMPLES="localhost" -SOURCE_CONTENT=http://localhost:7000/ +SOURCE_CONTENT=http://localhost:8100/ SOURCE_RUMBA=http://localhost:8000/ From 6abea057b0ea64efdd63b29cfc0a87c86bfefa38 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 18 Apr 2023 00:01:19 +0200 Subject: [PATCH 325/343] fixup! ci: remove xyz-build --- libs/constants/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/constants/index.js b/libs/constants/index.js index 973f9a656b5c..fdaab59268a4 100644 --- a/libs/constants/index.js +++ b/libs/constants/index.js @@ -111,7 +111,6 @@ export const CSP_DIRECTIVES = { "mdn.github.io", "live-samples.mdn.mozilla.net", "live-samples.mdn.allizom.net", - "live-samples.developer.allizom.xyz", "jsfiddle.net", "www.youtube-nocookie.com", From 37094d2adee8c4e0e5d366a995acf0a47e7877ec Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 18 Apr 2023 00:11:16 +0200 Subject: [PATCH 326/343] refactor(cloud-function): cleanup imports --- cloud-function/src/env.ts | 9 +++++---- cloud-function/src/handlers/handle-stripe-plans.ts | 9 +++------ cloud-function/src/handlers/proxy-kevel.ts | 4 ++-- cloud-function/src/handlers/proxy-telemetry.ts | 1 + cloud-function/src/headers.ts | 8 ++++---- cloud-function/src/middlewares/lowercase-pathname.ts | 8 ++++---- .../src/middlewares/redirect-fundamental.ts | 8 ++++---- .../src/middlewares/redirect-leading-slash.ts | 9 +++++---- cloud-function/src/middlewares/redirect-locale.ts | 8 ++++---- .../src/middlewares/redirect-moved-pages.ts | 10 +++++----- .../src/middlewares/redirect-trailing-slash.ts | 8 ++++---- cloud-function/src/middlewares/require-origin.ts | 1 + cloud-function/src/middlewares/resolve-index-html.ts | 12 +++++++----- cloud-function/src/utils.ts | 7 ++++--- 14 files changed, 53 insertions(+), 49 deletions(-) diff --git a/cloud-function/src/env.ts b/cloud-function/src/env.ts index 2ea9cd53d2c5..dd7c68f9c581 100644 --- a/cloud-function/src/env.ts +++ b/cloud-function/src/env.ts @@ -1,8 +1,9 @@ -import type express from "express"; -import dotenv from "dotenv"; 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"), }); @@ -26,7 +27,7 @@ export const ORIGIN_MAIN: string = process.env["ORIGIN_MAIN"] || "localhost"; export const ORIGIN_LIVE_SAMPLES: string = process.env["ORIGIN_LIVE_SAMPLES"] || "localhost"; -export function origin(req: express.Request): Origin { +export function origin(req: Request): Origin { switch (req.hostname) { case ORIGIN_MAIN: return Origin.main; @@ -42,7 +43,7 @@ export const SOURCE_CONTENT: string = export const SOURCE_API: string = process.env["SOURCE_API"] || "https://developer.allizom.org/"; -export function getOriginFromRequest(req: express.Request): Origin { +export function getOriginFromRequest(req: Request): Origin { if (req.hostname === ORIGIN_MAIN && !req.path.includes("/_sample_.")) { return Origin.main; } else if (req.hostname === ORIGIN_LIVE_SAMPLES) { diff --git a/cloud-function/src/handlers/handle-stripe-plans.ts b/cloud-function/src/handlers/handle-stripe-plans.ts index 5915e1ad2b38..c223a5fecc84 100644 --- a/cloud-function/src/handlers/handle-stripe-plans.ts +++ b/cloud-function/src/handlers/handle-stripe-plans.ts @@ -1,6 +1,6 @@ -import type express 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"; @@ -15,10 +15,7 @@ interface Result { plans: PlanResult; } -export async function handleStripePlans( - req: express.Request, - res: express.Response -) { +export async function handleStripePlans(req: Request, res: Response) { const lookupData = ORIGIN_MAIN === "developer.mozilla.org" ? prodLookup : stageLookup; diff --git a/cloud-function/src/handlers/proxy-kevel.ts b/cloud-function/src/handlers/proxy-kevel.ts index 769a9c9bd0ab..6a8c49e2d4ea 100644 --- a/cloud-function/src/handlers/proxy-kevel.ts +++ b/cloud-function/src/handlers/proxy-kevel.ts @@ -1,7 +1,7 @@ import * as url from "node:url"; -import type express from "express"; import { Client } from "@adzerk/decision-sdk"; +import type { Request, Response } from "express"; import { Coder } from "../internal/pong/index.js"; import { @@ -26,7 +26,7 @@ const handleGet = createPongGetHandler(client, coder, env); const handleClick = createPongClickHandler(coder); const handleViewed = createPongViewedHandler(coder); -export async function proxyKevel(req: express.Request, res: express.Response) { +export async function proxyKevel(req: Request, res: Response) { const countryCode = getRequestCountry(req); const userAgent = req.headers["user-agent"] ?? ""; diff --git a/cloud-function/src/handlers/proxy-telemetry.ts b/cloud-function/src/handlers/proxy-telemetry.ts index 65194b130e41..1040770df437 100644 --- a/cloud-function/src/handlers/proxy-telemetry.ts +++ b/cloud-function/src/handlers/proxy-telemetry.ts @@ -1,4 +1,5 @@ import { createProxyMiddleware, fixRequestBody } from "http-proxy-middleware"; + import { PROXY_TIMEOUT } from "../constants.js"; export const proxyTelemetry = createProxyMiddleware({ diff --git a/cloud-function/src/headers.ts b/cloud-function/src/headers.ts index cd6f48aba98f..0c9c6cd0d7fe 100644 --- a/cloud-function/src/headers.ts +++ b/cloud-function/src/headers.ts @@ -1,5 +1,5 @@ import type { IncomingMessage, ServerResponse } from "node:http"; -import type express from "express"; +import { Response } from "express"; import { CSP_VALUE } from "./internal/constants/index.js"; @@ -79,9 +79,9 @@ function parseContentType(value: unknown): string { } export function withResponseHeaders( - res: express.Response, + res: Response, options?: { csp?: boolean; xFrame?: boolean } -): express.Response { +): Response { setContentResponseHeaders( (name, value) => res.set(name, value), options ?? {} @@ -102,6 +102,6 @@ export function setContentResponseHeaders( ].forEach(([k, v]) => k && v && setHeader(k, v)); } -export function country(res: express.Request): string { +export function country(res: Response): string { return res.header("X-Appengine-Country") || ""; } diff --git a/cloud-function/src/middlewares/lowercase-pathname.ts b/cloud-function/src/middlewares/lowercase-pathname.ts index f992ba6395fa..65b3d4553403 100644 --- a/cloud-function/src/middlewares/lowercase-pathname.ts +++ b/cloud-function/src/middlewares/lowercase-pathname.ts @@ -1,11 +1,11 @@ import * as url from "node:url"; -import type express from "express"; +import { NextFunction, Request, Response } from "express"; export async function lowercasePathname( - req: express.Request, - _res: express.Response, - next: express.NextFunction + req: Request, + _res: Response, + next: NextFunction ) { const urlParsed = url.parse(req.url); if (urlParsed.pathname) { diff --git a/cloud-function/src/middlewares/redirect-fundamental.ts b/cloud-function/src/middlewares/redirect-fundamental.ts index aac3f21de0cb..fe0a1c57e7ab 100644 --- a/cloud-function/src/middlewares/redirect-fundamental.ts +++ b/cloud-function/src/middlewares/redirect-fundamental.ts @@ -1,13 +1,13 @@ -import type express 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: express.Request, - res: express.Response, - next: express.NextFunction + req: Request, + res: Response, + next: NextFunction ) { 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.ts index 64e41bd846e6..acd50cae75fa 100644 --- a/cloud-function/src/middlewares/redirect-leading-slash.ts +++ b/cloud-function/src/middlewares/redirect-leading-slash.ts @@ -1,4 +1,5 @@ -import type express from "express"; +import { NextFunction, Request, Response } from "express"; + import { redirect } from "../utils.js"; // If the URL was something like `https://domain/en-US/search/`, our code @@ -14,9 +15,9 @@ import { redirect } from "../utils.js"; // This essentially means that a request for `GET /////anything` becomes // 302 with `Location: /anything`. export async function redirectLeadingSlash( - req: express.Request, - res: express.Response, - next: express.NextFunction + req: Request, + res: Response, + next: NextFunction ) { const pathname = req.url; if (pathname.startsWith("//")) { diff --git a/cloud-function/src/middlewares/redirect-locale.ts b/cloud-function/src/middlewares/redirect-locale.ts index dec70253fc6f..413fcd46add6 100644 --- a/cloud-function/src/middlewares/redirect-locale.ts +++ b/cloud-function/src/middlewares/redirect-locale.ts @@ -1,4 +1,4 @@ -import type express 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,9 +7,9 @@ import { redirect } from "../utils.js"; const NEEDS_LOCALE = /^\/(?:docs|search|settings|signin|signup|plus)(?:$|\/)/; export async function redirectLocale( - req: express.Request, - res: express.Response, - next: express.NextFunction + req: Request, + res: Response, + next: NextFunction ) { const url = new URL(req.url, `${req.protocol}://${req.headers.host}`); const requestURI = url.pathname; diff --git a/cloud-function/src/middlewares/redirect-moved-pages.ts b/cloud-function/src/middlewares/redirect-moved-pages.ts index fed2aa78d693..284dd60a12fd 100644 --- a/cloud-function/src/middlewares/redirect-moved-pages.ts +++ b/cloud-function/src/middlewares/redirect-moved-pages.ts @@ -1,8 +1,8 @@ import { createRequire } from "node:module"; -import type express from "express"; -import { decodePath } from "../internal/slug-utils/index.js"; +import { NextFunction, Request, Response } from "express"; +import { decodePath } from "../internal/slug-utils/index.js"; import { THIRTY_DAYS } from "../constants.js"; import { redirect } from "../utils.js"; @@ -11,9 +11,9 @@ const REDIRECTS = require("../../redirects.json"); const REDIRECT_SUFFIXES = ["/index.json", "/bcd.json", ""]; export async function redirectMovedPages( - req: express.Request, - res: express.Response, - next: express.NextFunction + req: Request, + res: Response, + next: NextFunction ) { // Important: The requestURI may be URI-encoded. // Example: diff --git a/cloud-function/src/middlewares/redirect-trailing-slash.ts b/cloud-function/src/middlewares/redirect-trailing-slash.ts index 0369971941d3..397c8e423cdd 100644 --- a/cloud-function/src/middlewares/redirect-trailing-slash.ts +++ b/cloud-function/src/middlewares/redirect-trailing-slash.ts @@ -1,4 +1,4 @@ -import type express from "express"; +import { NextFunction, Request, Response } from "express"; import { THIRTY_DAYS } from "../constants.js"; import { VALID_LOCALES } from "../internal/constants/index.js"; @@ -26,9 +26,9 @@ const LEGACY_URI_NEEDING_TRAILING_SLASH = new RegExp( ); export async function redirectTrailingSlash( - req: express.Request, - res: express.Response, - next: express.NextFunction + req: Request, + res: Response, + next: NextFunction ) { const url = new URL(req.url, `${req.protocol}://${req.headers.host}`); let requestURI = url.pathname; diff --git a/cloud-function/src/middlewares/require-origin.ts b/cloud-function/src/middlewares/require-origin.ts index 91f29d12c49f..e03457a45255 100644 --- a/cloud-function/src/middlewares/require-origin.ts +++ b/cloud-function/src/middlewares/require-origin.ts @@ -1,4 +1,5 @@ import type { NextFunction, Request, Response } from "express"; + import { Origin, getOriginFromRequest } from "../env.js"; export function requireOrigin(...expectedOrigins: Origin[]) { diff --git a/cloud-function/src/middlewares/resolve-index-html.ts b/cloud-function/src/middlewares/resolve-index-html.ts index acb5933d3c50..0f88e9d81416 100644 --- a/cloud-function/src/middlewares/resolve-index-html.ts +++ b/cloud-function/src/middlewares/resolve-index-html.ts @@ -1,11 +1,13 @@ -import type express from "express"; -import { slugToFolder } from "../internal/slug-utils/index.js"; import * as path from "node:path"; +import { NextFunction, Request, Response } from "express"; + +import { slugToFolder } from "../internal/slug-utils/index.js"; + export async function resolveIndexHTML( - req: express.Request, - _res: express.Response, - next: express.NextFunction + req: Request, + _res: Response, + next: NextFunction ) { let resolvedUrl = slugToFolder(req.url); if (path.extname(resolvedUrl) === "") { diff --git a/cloud-function/src/utils.ts b/cloud-function/src/utils.ts index 0cdade49240e..158df6187067 100644 --- a/cloud-function/src/utils.ts +++ b/cloud-function/src/utils.ts @@ -1,7 +1,8 @@ -import type express from "express"; +import { Request, Response } from "express"; + import { DEFAULT_COUNTRY } from "./constants.js"; -export function getRequestCountry(req: express.Request): string { +export function getRequestCountry(req: Request): string { const value = req.headers["cloudfront-viewer-country"]; if (typeof value === "string" && value !== "ZZ") { @@ -12,7 +13,7 @@ export function getRequestCountry(req: express.Request): string { } export function redirect( - res: express.Response, + res: Response, location: string, { status = 302, cacheControlSeconds = 0 } = {} ): void { From 72d549c211bce8b0371549e198611d7b84967cc2 Mon Sep 17 00:00:00 2001 From: Florian Dieminger Date: Mon, 17 Apr 2023 23:47:18 +0200 Subject: [PATCH 327/343] workaround(content-proxy): 404 on slugs with dot --- cloud-function/src/handlers/proxy-content.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cloud-function/src/handlers/proxy-content.ts b/cloud-function/src/handlers/proxy-content.ts index d7941b399b85..8c9cf01b14be 100644 --- a/cloud-function/src/handlers/proxy-content.ts +++ b/cloud-function/src/handlers/proxy-content.ts @@ -26,7 +26,11 @@ export const proxyContent = createProxyMiddleware({ async (responseBuffer, proxyRes, req, res) => { withContentResponseHeaders(proxyRes, req, res); if (proxyRes.statusCode === 404) { - if (!notFoundBuffer) { + const tryHtml = await fetch(`${target}${req.url}/index.html`); + if (tryHtml.ok) { + res.statusCode = 200; + return Buffer.from(await tryHtml.arrayBuffer()); + } else if (!notFoundBuffer) { const response = await fetch(`${target}${NOT_FOUND_PATH}`); notFoundBuffer = await response.arrayBuffer(); } From f55e87835054d079609b85de08461f02042cfc5e Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 18 Apr 2023 00:19:26 +0200 Subject: [PATCH 328/343] chore(cloud-function): remove unused function --- cloud-function/src/headers.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cloud-function/src/headers.ts b/cloud-function/src/headers.ts index 0c9c6cd0d7fe..5a9da0493d39 100644 --- a/cloud-function/src/headers.ts +++ b/cloud-function/src/headers.ts @@ -101,7 +101,3 @@ export function setContentResponseHeaders( ...(xFrame ? [["X-Frame-Options", "DENY"]] : []), ].forEach(([k, v]) => k && v && setHeader(k, v)); } - -export function country(res: Response): string { - return res.header("X-Appengine-Country") || ""; -} From 53ee40173749884d18daf0ef53e24b2c49163020 Mon Sep 17 00:00:00 2001 From: Florian Dieminger Date: Tue, 18 Apr 2023 00:37:22 +0200 Subject: [PATCH 329/343] workaround(content-proxy): fix workaround --- cloud-function/src/handlers/proxy-content.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud-function/src/handlers/proxy-content.ts b/cloud-function/src/handlers/proxy-content.ts index 8c9cf01b14be..d2e3687f9b36 100644 --- a/cloud-function/src/handlers/proxy-content.ts +++ b/cloud-function/src/handlers/proxy-content.ts @@ -26,9 +26,10 @@ export const proxyContent = createProxyMiddleware({ async (responseBuffer, proxyRes, req, res) => { withContentResponseHeaders(proxyRes, req, res); if (proxyRes.statusCode === 404) { - const tryHtml = await fetch(`${target}${req.url}/index.html`); + const tryHtml = await fetch(`${target}${req.url?.slice(1)}/index.html`); if (tryHtml.ok) { res.statusCode = 200; + res.setHeader("Content-Type", "text/html"); return Buffer.from(await tryHtml.arrayBuffer()); } else if (!notFoundBuffer) { const response = await fetch(`${target}${NOT_FOUND_PATH}`); From 13092b5414ff646fe9115ff2ec36cbc8d527100c Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 18 Apr 2023 01:02:50 +0200 Subject: [PATCH 330/343] fix(cloud-function): use path as slug, not full url with query --- cloud-function/src/middlewares/resolve-index-html.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cloud-function/src/middlewares/resolve-index-html.ts b/cloud-function/src/middlewares/resolve-index-html.ts index 0f88e9d81416..ad8c9fc781fd 100644 --- a/cloud-function/src/middlewares/resolve-index-html.ts +++ b/cloud-function/src/middlewares/resolve-index-html.ts @@ -9,8 +9,9 @@ export async function resolveIndexHTML( _res: Response, next: NextFunction ) { - let resolvedUrl = slugToFolder(req.url); - if (path.extname(resolvedUrl) === "") { + let resolvedUrl = slugToFolder(req.path); + const ext = path.extname(resolvedUrl); + if (ext === "") { resolvedUrl = path.join(resolvedUrl, "index.html"); } req.url = resolvedUrl; From 4cad3cc318327310f46a5b3760112cac67612d57 Mon Sep 17 00:00:00 2001 From: Claas Augner <495429+caugner@users.noreply.github.com> Date: Tue, 18 Apr 2023 10:46:18 +0200 Subject: [PATCH 331/343] fix(cloud-function): reuse resolveIndexHtml for live samples This replaces `:` in the URL, for instance. --- cloud-function/src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud-function/src/app.ts b/cloud-function/src/app.ts index f2b57045fd8e..91dbb7d02fd2 100644 --- a/cloud-function/src/app.ts +++ b/cloud-function/src/app.ts @@ -43,7 +43,7 @@ router.get("/", requireOrigin(Origin.main), redirectLocale); router.get( "/[^/]+/docs/*/_sample_.*.html", requireOrigin(Origin.liveSamples), - lowercasePathname, + resolveIndexHTML, proxyContent ); router.get( From c77996ba236878b7fc4d8a4976f570aff27541b5 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 18 Apr 2023 11:30:09 +0200 Subject: [PATCH 332/343] fixup! fix(cloud-function): reuse resolveIndexHtml for live samples --- cloud-function/src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud-function/src/app.ts b/cloud-function/src/app.ts index 91dbb7d02fd2..784d64295585 100644 --- a/cloud-function/src/app.ts +++ b/cloud-function/src/app.ts @@ -49,7 +49,7 @@ router.get( router.get( "/[^/]+/docs/*/*.(png|jpeg|jpg|gif|svg|webp)", requireOrigin(Origin.main, Origin.liveSamples), - lowercasePathname, + resolveIndexHTML, proxyContent ); router.get( From ac8cd76b540b163c6b81fb51ca4c80b14a171cba Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 18 Apr 2023 13:39:46 +0200 Subject: [PATCH 333/343] refactor(cloud-function): use array for api paths --- cloud-function/src/app.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cloud-function/src/app.ts b/cloud-function/src/app.ts index 784d64295585..1fe2d182cd66 100644 --- a/cloud-function/src/app.ts +++ b/cloud-function/src/app.ts @@ -25,10 +25,11 @@ router.all( requireOrigin(Origin.main), handleStripePlans ); -router.all("/api/*", requireOrigin(Origin.main), proxyApi); -router.all("/admin-api/*", requireOrigin(Origin.main), proxyApi); -router.all("/events/fxa/*", requireOrigin(Origin.main), proxyApi); -router.all("/users/fxa/*", requireOrigin(Origin.main), proxyApi); +router.all( + ["/api/*", "/admin-api/*", "/events/fxa/*", "/users/fxa/*"], + requireOrigin(Origin.main), + proxyApi +); router.all( "/submit/mdn-yari/*", requireOrigin(Origin.main), From 8e7a8d18fd8202568d3d6b4a6b4c0cd620e109a4 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 18 Apr 2023 15:47:41 +0200 Subject: [PATCH 334/343] fix(cloud-function): use res.{send => sendStatus} --- cloud-function/src/middlewares/not-found.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud-function/src/middlewares/not-found.ts b/cloud-function/src/middlewares/not-found.ts index 4c32ac8a0ae0..7e3a099966dc 100644 --- a/cloud-function/src/middlewares/not-found.ts +++ b/cloud-function/src/middlewares/not-found.ts @@ -1,5 +1,5 @@ import type { Request, Response } from "express"; export async function notFound(_req: Request, res: Response) { - res.send(404); + res.sendStatus(404); } From 0766f93a783fcbf8f5c54fa042aaff136c0e6e1a Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 18 Apr 2023 15:57:34 +0200 Subject: [PATCH 335/343] chore(cloud-function): serve /assets as is --- cloud-function/src/app.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cloud-function/src/app.ts b/cloud-function/src/app.ts index 1fe2d182cd66..a962ea9b767f 100644 --- a/cloud-function/src/app.ts +++ b/cloud-function/src/app.ts @@ -38,8 +38,11 @@ router.all( ); router.all("/pong/*", requireOrigin(Origin.main), express.json(), proxyKevel); router.all("/pimg/*", requireOrigin(Origin.main), proxyKevel); -router.get("/sitemaps/*", requireOrigin(Origin.main), proxyContent); -router.get("/static/*", requireOrigin(Origin.main), proxyContent); +router.get( + ["/assets/*", "/sitemaps/*", "/static/*"], + requireOrigin(Origin.main), + proxyContent +); router.get("/", requireOrigin(Origin.main), redirectLocale); router.get( "/[^/]+/docs/*/_sample_.*.html", From 183425c0d53ef76431b568a584102cabad690f56 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 18 Apr 2023 15:59:54 +0200 Subject: [PATCH 336/343] chore(cloud-function): serve root assets as is --- cloud-function/src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud-function/src/app.ts b/cloud-function/src/app.ts index a962ea9b767f..f093c56b2cef 100644 --- a/cloud-function/src/app.ts +++ b/cloud-function/src/app.ts @@ -39,7 +39,7 @@ router.all( router.all("/pong/*", requireOrigin(Origin.main), express.json(), proxyKevel); router.all("/pimg/*", requireOrigin(Origin.main), proxyKevel); router.get( - ["/assets/*", "/sitemaps/*", "/static/*"], + ["/assets/*", "/sitemaps/*", "/static/*", "/[^/]+.[^/]+"], requireOrigin(Origin.main), proxyContent ); From 704447a2e4a27ed904deed97395e16d5b726675e Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 18 Apr 2023 16:21:10 +0200 Subject: [PATCH 337/343] fix(cloud-function): add index.html to everything except assets --- cloud-function/src/middlewares/resolve-index-html.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cloud-function/src/middlewares/resolve-index-html.ts b/cloud-function/src/middlewares/resolve-index-html.ts index ad8c9fc781fd..5bd0cd6bab6b 100644 --- a/cloud-function/src/middlewares/resolve-index-html.ts +++ b/cloud-function/src/middlewares/resolve-index-html.ts @@ -4,14 +4,21 @@ import { NextFunction, Request, Response } from "express"; import { slugToFolder } from "../internal/slug-utils/index.js"; +// These are the only extensions in client/build/*/docs/*. +// `find client/build -type f | grep docs | xargs basename | sed 's/.*\.\([^.]*\)$/\1/' | sort | uniq` +const ASSET_REGEXP = /\.(gif|html|jpeg|jpg|json|png|svg|txt)$/i; + +function isAsset(url: string) { + return ASSET_REGEXP.test(url); +} + export async function resolveIndexHTML( req: Request, _res: Response, next: NextFunction ) { let resolvedUrl = slugToFolder(req.path); - const ext = path.extname(resolvedUrl); - if (ext === "") { + if (!isAsset(resolvedUrl)) { resolvedUrl = path.join(resolvedUrl, "index.html"); } req.url = resolvedUrl; From a6097253d4c4a609a955100f79656fb4e3985324 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 18 Apr 2023 16:31:25 +0200 Subject: [PATCH 338/343] fix(cloud-function): parse/format url in resolveIndexHTML --- .../src/middlewares/resolve-index-html.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/cloud-function/src/middlewares/resolve-index-html.ts b/cloud-function/src/middlewares/resolve-index-html.ts index 5bd0cd6bab6b..d8d16028c447 100644 --- a/cloud-function/src/middlewares/resolve-index-html.ts +++ b/cloud-function/src/middlewares/resolve-index-html.ts @@ -1,4 +1,5 @@ import * as path from "node:path"; +import * as url from "node:url"; import { NextFunction, Request, Response } from "express"; @@ -17,13 +18,17 @@ export async function resolveIndexHTML( _res: Response, next: NextFunction ) { - let resolvedUrl = slugToFolder(req.path); - if (!isAsset(resolvedUrl)) { - resolvedUrl = path.join(resolvedUrl, "index.html"); + const urlParsed = url.parse(req.url); + if (urlParsed.pathname) { + let pathname = slugToFolder(urlParsed.pathname); + if (!isAsset(pathname)) { + pathname = path.join(pathname, "index.html"); + } + urlParsed.pathname = pathname; + req.url = url.format(urlParsed); + // Workaround for http-proxy-middleware v2 using `req.originalUrl`. + // See: https://github.com/chimurai/http-proxy-middleware/pull/731 + req.originalUrl = req.url; } - req.url = resolvedUrl; - // Workaround for http-proxy-middleware v2 using `req.originalUrl`. - // See: https://github.com/chimurai/http-proxy-middleware/pull/731 - req.originalUrl = req.url; next(); } From 2f44e686ab02a3fd6bd5664ada133d8c9baba5a2 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 18 Apr 2023 16:31:57 +0200 Subject: [PATCH 339/343] fix(cloud-function): touch req.url only if url has pathname --- cloud-function/src/middlewares/lowercase-pathname.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cloud-function/src/middlewares/lowercase-pathname.ts b/cloud-function/src/middlewares/lowercase-pathname.ts index 65b3d4553403..3e87a55589d9 100644 --- a/cloud-function/src/middlewares/lowercase-pathname.ts +++ b/cloud-function/src/middlewares/lowercase-pathname.ts @@ -10,10 +10,10 @@ export async function lowercasePathname( const urlParsed = url.parse(req.url); if (urlParsed.pathname) { urlParsed.pathname = urlParsed.pathname.toLowerCase(); + req.url = url.format(urlParsed); + // Workaround for http-proxy-middleware v2 using `req.originalUrl`. + // See: https://github.com/chimurai/http-proxy-middleware/pull/731 + req.originalUrl = req.url; } - req.url = url.format(urlParsed); - // Workaround for http-proxy-middleware v2 using `req.originalUrl`. - // See: https://github.com/chimurai/http-proxy-middleware/pull/731 - req.originalUrl = req.url; next(); } From ef8ee2d1594f0707086df42dc020e216183ff58e Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 18 Apr 2023 16:40:03 +0200 Subject: [PATCH 340/343] chore(cloud-function): remove compression This was not the reason `/submit/mdn-yari` was broken. --- cloud-function/package-lock.json | 60 -------------------------------- cloud-function/package.json | 2 -- cloud-function/src/app.ts | 8 +---- 3 files changed, 1 insertion(+), 69 deletions(-) diff --git a/cloud-function/package-lock.json b/cloud-function/package-lock.json index 9d9957007575..8b9cc145dd4b 100644 --- a/cloud-function/package-lock.json +++ b/cloud-function/package-lock.json @@ -18,7 +18,6 @@ "@yari-internal/pong": "file:src/internal/pong", "@yari-internal/slug-utils": "file:src/internal/slug-utils", "accept-language-parser": "^1.5.0", - "compression": "^1.7.4", "dotenv": "^16.0.3", "express": "^4.18.2", "http-proxy-middleware": "^2.0.6", @@ -27,7 +26,6 @@ "devDependencies": { "@swc/core": "^1.3.38", "@types/accept-language-parser": "^1.5.3", - "@types/compression": "^1.7.2", "@types/http-proxy": "^1.17.10", "@types/http-server": "^0.12.1", "http-proxy": "^1.18.1", @@ -544,15 +542,6 @@ "@types/node": "*" } }, - "node_modules/@types/compression": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.2.tgz", - "integrity": "sha512-lwEL4M/uAGWngWFLSG87ZDr2kLrbuR8p7X+QZB1OQlT+qkHsCPDVFnHPyXf4Vyl4yDDorNY+mAhosxkCvppatg==", - "dev": true, - "dependencies": { - "@types/express": "*" - } - }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -996,47 +985,6 @@ "node": ">= 0.8" } }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2602,14 +2550,6 @@ "node": ">= 0.8" } }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", diff --git a/cloud-function/package.json b/cloud-function/package.json index 4a766377178b..5f3a99a7e20b 100644 --- a/cloud-function/package.json +++ b/cloud-function/package.json @@ -35,7 +35,6 @@ "@yari-internal/pong": "file:src/internal/pong", "@yari-internal/slug-utils": "file:src/internal/slug-utils", "accept-language-parser": "^1.5.0", - "compression": "^1.7.4", "dotenv": "^16.0.3", "express": "^4.18.2", "http-proxy-middleware": "^2.0.6", @@ -44,7 +43,6 @@ "devDependencies": { "@swc/core": "^1.3.38", "@types/accept-language-parser": "^1.5.3", - "@types/compression": "^1.7.2", "@types/http-proxy": "^1.17.10", "@types/http-server": "^0.12.1", "http-proxy": "^1.18.1", diff --git a/cloud-function/src/app.ts b/cloud-function/src/app.ts index f093c56b2cef..e802f83f9978 100644 --- a/cloud-function/src/app.ts +++ b/cloud-function/src/app.ts @@ -1,6 +1,5 @@ import express, { Request, Response } from "express"; import { Router } from "express"; -import compression from "compression"; import { Origin } from "./env.js"; import { proxyContent } from "./handlers/proxy-content.js"; @@ -30,12 +29,7 @@ router.all( requireOrigin(Origin.main), proxyApi ); -router.all( - "/submit/mdn-yari/*", - requireOrigin(Origin.main), - compression(), - proxyTelemetry -); +router.all("/submit/mdn-yari/*", requireOrigin(Origin.main), proxyTelemetry); router.all("/pong/*", requireOrigin(Origin.main), express.json(), proxyKevel); router.all("/pimg/*", requireOrigin(Origin.main), proxyKevel); router.get( From f1e4a7d6005070b7df0a81251a6d3a7854be36c8 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 18 Apr 2023 16:47:16 +0200 Subject: [PATCH 341/343] refactor(cloud-function): use res.{status => sendStatus} --- cloud-function/src/handlers/handle-stripe-plans.ts | 4 ++-- cloud-function/src/handlers/proxy-kevel.ts | 12 ++++++------ cloud-function/src/middlewares/not-found.ts | 2 +- cloud-function/src/middlewares/require-origin.ts | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cloud-function/src/handlers/handle-stripe-plans.ts b/cloud-function/src/handlers/handle-stripe-plans.ts index c223a5fecc84..43e4281be772 100644 --- a/cloud-function/src/handlers/handle-stripe-plans.ts +++ b/cloud-function/src/handlers/handle-stripe-plans.ts @@ -26,7 +26,7 @@ export async function handleStripePlans(req: Request, res: Response) { const supportedCurrency = lookupData.countryToCurrency[countryCode]; if (!supportedCurrency) { - return res.status(404).end(); + return res.sendStatus(404).end(); } const acceptLanguage = typeof localeHeader === "string" ? localeHeader : null; @@ -48,7 +48,7 @@ export async function handleStripePlans(req: Request, res: Response) { const plans = lookupData.langCurrencyToPlans[key]; if (!plans) { - return res.status(500).end(); + return res.sendStatus(500).end(); } const planResult: PlanResult = {}; diff --git a/cloud-function/src/handlers/proxy-kevel.ts b/cloud-function/src/handlers/proxy-kevel.ts index 6a8c49e2d4ea..e8d241fc6f1e 100644 --- a/cloud-function/src/handlers/proxy-kevel.ts +++ b/cloud-function/src/handlers/proxy-kevel.ts @@ -37,7 +37,7 @@ export async function proxyKevel(req: Request, res: Response) { if (pathname === "/pong/get") { if (req.method !== "POST") { - return res.status(405).end(); + return res.sendStatus(405).end(); } const { body } = req; @@ -54,7 +54,7 @@ export async function proxyKevel(req: Request, res: Response) { .end(JSON.stringify(payload)); } else if (req.path === "/pong/click") { if (req.method !== "GET") { - return res.status(405).end(); + return res.sendStatus(405).end(); } const params = new URLSearchParams(search); try { @@ -62,19 +62,19 @@ export async function proxyKevel(req: Request, res: Response) { if (location && (status === 301 || status === 302)) { return res.redirect(location); } else { - return res.status(502).end(); + return res.sendStatus(502).end(); } } catch (e) { console.error(e); } } else if (pathname === "/pong/viewed") { if (req.method !== "POST") { - return res.status(405).end(); + return res.sendStatus(405).end(); } const params = new URLSearchParams(search); try { await handleViewed(params); - return res.status(201).end(); + return res.sendStatus(201).end(); } catch (e) { console.error(e); } @@ -83,7 +83,7 @@ export async function proxyKevel(req: Request, res: Response) { decodeURIComponent(pathname.substring("/pimg/".length)) ); if (!src) { - return res.status(400).end(); + return res.sendStatus(400).end(); } const { buf, contentType } = await fetchImage(src); return res diff --git a/cloud-function/src/middlewares/not-found.ts b/cloud-function/src/middlewares/not-found.ts index 7e3a099966dc..71f0f6e17006 100644 --- a/cloud-function/src/middlewares/not-found.ts +++ b/cloud-function/src/middlewares/not-found.ts @@ -1,5 +1,5 @@ import type { Request, Response } from "express"; export async function notFound(_req: Request, res: Response) { - res.sendStatus(404); + res.sendStatus(404).end(); } diff --git a/cloud-function/src/middlewares/require-origin.ts b/cloud-function/src/middlewares/require-origin.ts index e03457a45255..0e2f2f58280e 100644 --- a/cloud-function/src/middlewares/require-origin.ts +++ b/cloud-function/src/middlewares/require-origin.ts @@ -9,7 +9,7 @@ export function requireOrigin(...expectedOrigins: Origin[]) { if (expectedOrigins.includes(actualOrigin)) { return next(); } else { - return res.status(404).end(); + return res.sendStatus(404).end(); } }; } From 69e897de3ae58c74b2ea0c020425d80a17bcb0ae Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 18 Apr 2023 17:01:27 +0200 Subject: [PATCH 342/343] fix(cloud-function): do not apply 404 fallback to live-samples --- cloud-function/src/handlers/proxy-content.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud-function/src/handlers/proxy-content.ts b/cloud-function/src/handlers/proxy-content.ts index d2e3687f9b36..4599aaf7c244 100644 --- a/cloud-function/src/handlers/proxy-content.ts +++ b/cloud-function/src/handlers/proxy-content.ts @@ -25,7 +25,7 @@ export const proxyContent = createProxyMiddleware({ onProxyRes: responseInterceptor( async (responseBuffer, proxyRes, req, res) => { withContentResponseHeaders(proxyRes, req, res); - if (proxyRes.statusCode === 404) { + if (proxyRes.statusCode === 404 && !req.url?.includes("/_sample_.")) { const tryHtml = await fetch(`${target}${req.url?.slice(1)}/index.html`); if (tryHtml.ok) { res.statusCode = 200; From 7d1f559b42f489f4bd661cd9f0b892c1c8d9ade9 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 18 Apr 2023 17:03:40 +0200 Subject: [PATCH 343/343] refactor(cloud-function): extract isLiveSampleURL() --- cloud-function/src/handlers/proxy-content.ts | 3 ++- cloud-function/src/headers.ts | 7 ++++--- cloud-function/src/utils.ts | 4 ++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/cloud-function/src/handlers/proxy-content.ts b/cloud-function/src/handlers/proxy-content.ts index 4599aaf7c244..6001e914cbe7 100644 --- a/cloud-function/src/handlers/proxy-content.ts +++ b/cloud-function/src/handlers/proxy-content.ts @@ -7,6 +7,7 @@ import { import { withContentResponseHeaders } from "../headers.js"; import { Source, sourceUri } from "../env.js"; import { PROXY_TIMEOUT } from "../constants.js"; +import { isLiveSampleURL } from "../utils.js"; const NOT_FOUND_PATH = "en-us/_spas/404.html"; @@ -25,7 +26,7 @@ export const proxyContent = createProxyMiddleware({ onProxyRes: responseInterceptor( async (responseBuffer, proxyRes, req, res) => { withContentResponseHeaders(proxyRes, req, res); - if (proxyRes.statusCode === 404 && !req.url?.includes("/_sample_.")) { + if (proxyRes.statusCode === 404 && !isLiveSampleURL(req.url ?? "")) { const tryHtml = await fetch(`${target}${req.url?.slice(1)}/index.html`); if (tryHtml.ok) { res.statusCode = 200; diff --git a/cloud-function/src/headers.ts b/cloud-function/src/headers.ts index 5a9da0493d39..1b051cebc9a9 100644 --- a/cloud-function/src/headers.ts +++ b/cloud-function/src/headers.ts @@ -2,6 +2,7 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import { Response } from "express"; import { CSP_VALUE } from "./internal/constants/index.js"; +import { isLiveSampleURL } from "./utils.js"; const HASHED_MAX_AGE = 60 * 60 * 24 * 365; const DEFAULT_MAX_AGE = 60 * 60 * 24; @@ -24,15 +25,15 @@ export function withContentResponseHeaders( const url = req.url ?? ""; - const isLiveSampleURI = url.includes("/_sample_.") ?? false; + const isLiveSample = isLiveSampleURL(url); setContentResponseHeaders((name, value) => res.setHeader(name, value), { csp: - !isLiveSampleURI && + !isLiveSample && parseContentType(proxyRes.headers["content-type"]).startsWith( "text/html" ), - xFrame: !isLiveSampleURI, + xFrame: !isLiveSample, }); if (req.url?.endsWith("/sitemap.xml.gz")) { diff --git a/cloud-function/src/utils.ts b/cloud-function/src/utils.ts index 158df6187067..2e0cf39e608f 100644 --- a/cloud-function/src/utils.ts +++ b/cloud-function/src/utils.ts @@ -38,3 +38,7 @@ export function redirect( res.set("Cache-Control", cacheControlValue).redirect(status, newLocation); } + +export function isLiveSampleURL(url: string) { + return url.includes("/_sample_."); +}