diff --git a/.prettierignore b/.prettierignore index 4bbd22f16d7..996b93c1a8e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,8 +6,6 @@ src/db/**/*.js src/emails/**/*.js # TODO NEXT.JS MIGRATION: -# These files are remnants of our Express app: -src/appConstants.js # These should be ignored anyway coverage/ diff --git a/jest.config.cjs b/jest.config.cjs index cd8543df1f5..cbcbf386333 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -43,8 +43,6 @@ const customJestConfig = { "/src/apiMocks/mockData.ts", "/src/(.+).stories.(ts|tsx)", "/.storybook/", - // Old, pre-Next.js code assumed to be working: - "/src/appConstants.js", ], // Indicates which provider should be used to instrument code for coverage diff --git a/jest.setup.ts b/jest.setup.ts index d27a7c8b3c4..0b8b2166293 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -47,13 +47,11 @@ afterEach(() => { global.TextEncoder = TextEncoder; -// Jest doesn't like the top-level await in AppConstants, so we mock it. In -// time we can hopefully phase out the entire file and just use dotenv-flow -// and process.env directly. -jest.mock("./src/appConstants.js", () => { +// Jest doesn't like the top-level await in envVars.ts, so we mock it. +jest.mock("./src/envVars", () => { // eslint-disable-next-line @typescript-eslint/no-var-requires require("dotenv-flow").config(); return { - ...process.env, + getEnvVarsOrThrow: () => process.env, }; }); diff --git a/src/app/api/utils/auth.tsx b/src/app/api/utils/auth.tsx index af86dfc67ee..994d7e751ca 100644 --- a/src/app/api/utils/auth.tsx +++ b/src/app/api/utils/auth.tsx @@ -7,7 +7,6 @@ import { AuthOptions, Profile as FxaProfile, User } from "next-auth"; import { SubscriberRow } from "knex/types/tables"; import { logger } from "../../functions/server/logging"; -import AppConstants from "../../../appConstants.js"; import { getSubscriberByFxaUid, updateFxAData, @@ -26,6 +25,15 @@ import { SerializedSubscriber } from "../../../next-auth"; import { record } from "../../functions/server/glean"; import { renderEmail } from "../../../emails/renderEmail"; import { SignupReportEmail } from "../../../emails/templates/signupReport/SignupReportEmail"; +import { getEnvVarsOrThrow } from "../../../envVars"; + +const envVars = getEnvVarsOrThrow([ + "OAUTH_AUTHORIZATION_URI", + "OAUTH_TOKEN_URI", + "OAUTH_CLIENT_ID", + "OAUTH_CLIENT_SECRET", + "OAUTH_PROFILE_URI", +]); const fxaProviderConfig: OAuthConfig = { // As per https://mozilla.slack.com/archives/C4D36CAJW/p1683642497940629?thread_ts=1683642325.465929&cid=C4D36CAJW, @@ -36,7 +44,7 @@ const fxaProviderConfig: OAuthConfig = { name: "Mozilla accounts", type: "oauth", authorization: { - url: AppConstants.OAUTH_AUTHORIZATION_URI, + url: envVars.OAUTH_AUTHORIZATION_URI, params: { scope: "profile https://identity.mozilla.com/account/subscriptions", access_type: "offline", @@ -45,11 +53,11 @@ const fxaProviderConfig: OAuthConfig = { max_age: 0, }, }, - token: AppConstants.OAUTH_TOKEN_URI, - // userinfo: AppConstants.OAUTH_PROFILE_URI, + token: envVars.OAUTH_TOKEN_URI, + // userinfo: envVars.OAUTH_PROFILE_URI, userinfo: { request: async (context) => { - const response = await fetch(AppConstants.OAUTH_PROFILE_URI, { + const response = await fetch(envVars.OAUTH_PROFILE_URI, { headers: { Authorization: `Bearer ${context.tokens.access_token ?? ""}`, }, @@ -57,8 +65,8 @@ const fxaProviderConfig: OAuthConfig = { return (await response.json()) as FxaProfile; }, }, - clientId: AppConstants.OAUTH_CLIENT_ID, - clientSecret: AppConstants.OAUTH_CLIENT_SECRET, + clientId: envVars.OAUTH_CLIENT_ID, + clientSecret: envVars.OAUTH_CLIENT_SECRET, // Parse data returned by FxA's /userinfo/ profile: (profile) => { return convertFxaProfile(profile); @@ -309,6 +317,6 @@ export function bearerToken(req: NextRequest) { } export function isAdmin(email: string) { - const admins = AppConstants.ADMINS?.split(",") ?? []; + const admins = (process.env.ADMINS ?? "").split(",") ?? []; return admins.includes(email); } diff --git a/src/app/api/v1/fxa-rp-events/route.ts b/src/app/api/v1/fxa-rp-events/route.ts index 8cd1b27c45f..13d80013e24 100644 --- a/src/app/api/v1/fxa-rp-events/route.ts +++ b/src/app/api/v1/fxa-rp-events/route.ts @@ -22,7 +22,6 @@ import { } from "../../../functions/server/onerep"; import { bearerToken } from "../../utils/auth"; import { revokeOAuthTokens } from "../../../../utils/fxa"; -import appConstants from "../../../../appConstants"; import { changeSubscription } from "../../../functions/server/changeSubscription"; import { deleteAccount } from "../../../functions/server/deleteAccount"; import { record } from "../../../functions/server/glean"; @@ -43,7 +42,7 @@ const MONITOR_PREMIUM_CAPABILITY = "monitor"; * @returns {Promise | undefined>} keys an array of FxA JWT keys */ const getJwtPubKey = async () => { - const jwtKeyUri = `${appConstants.OAUTH_ACCOUNT_URI}/jwks`; + const jwtKeyUri = `${process.env.OAUTH_ACCOUNT_URI}/jwks`; try { const response = await fetch(jwtKeyUri, { headers: { diff --git a/src/app/api/v1/user/email/route.ts b/src/app/api/v1/user/email/route.ts index b36621c310b..c2c9ea8ad6d 100644 --- a/src/app/api/v1/user/email/route.ts +++ b/src/app/api/v1/user/email/route.ts @@ -4,7 +4,6 @@ import { getToken } from "next-auth/jwt"; import { NextRequest, NextResponse } from "next/server"; -import AppConstants from "../../../../../appConstants"; import { getSubscriberByFxaUid } from "../../../../../db/tables/subscribers"; import { addSubscriberUnverifiedEmailHash } from "../../../../../db/tables/emailAddresses"; @@ -108,6 +107,6 @@ export async function POST(req: NextRequest) { } } else { // Not Signed in, redirect to home - return NextResponse.redirect(AppConstants.SERVER_URL, 301); + return NextResponse.redirect(process.env.SERVER_URL ?? "/", 301); } } diff --git a/src/app/api/v1/user/remove-email/route.ts b/src/app/api/v1/user/remove-email/route.ts index c3923cac284..db5ea0f6da0 100644 --- a/src/app/api/v1/user/remove-email/route.ts +++ b/src/app/api/v1/user/remove-email/route.ts @@ -6,7 +6,6 @@ import { getToken } from "next-auth/jwt"; import { NextRequest, NextResponse } from "next/server"; import { logger } from "../../../../functions/server/logging"; -import AppConstants from "../../../../../appConstants"; import { getSubscriberByFxaUid, deleteResolutionsWithEmail, @@ -50,7 +49,7 @@ export async function POST(req: NextRequest) { existingEmail.email, ); return NextResponse.redirect( - AppConstants.SERVER_URL + "/user/settings", + process.env.SERVER_URL + "/user/settings", 301, ); } catch (e) { diff --git a/src/appConstants.js b/src/appConstants.js deleted file mode 100644 index 9069e2ec5b5..00000000000 --- a/src/appConstants.js +++ /dev/null @@ -1,72 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -if (typeof process.env.NEXT_RUNTIME === "undefined" && typeof process.env.STORYBOOK === "undefined") { - // Next.js already loads env vars by itself, and dotenv-flow will throw an - // error if loaded in that context (about `fs` not existing), so only load - // it if we're not running in a Next.js-context (e.g. cron jobs): - await import("dotenv-flow/config"); -} - -// TODO: these vars were copy/pasted from the old app-constants.js and should be cleaned up -const requiredEnvVars = [ - 'ADMINS', - 'APP_ENV', - 'DATABASE_URL', - 'DELETE_UNVERIFIED_SUBSCRIBERS_TIMER', - 'EMAIL_FROM', - 'HIBP_API_ROOT', - 'HIBP_KANON_API_ROOT', - 'HIBP_KANON_API_TOKEN', - 'HIBP_NOTIFY_TOKEN', - 'HIBP_THROTTLE_DELAY', - 'HIBP_THROTTLE_MAX_TRIES', - 'FXA_SETTINGS_URL', - 'NODE_ENV', - 'OAUTH_ACCOUNT_URI', - 'OAUTH_AUTHORIZATION_URI', - 'OAUTH_CLIENT_ID', - 'OAUTH_CLIENT_SECRET', - 'OAUTH_PROFILE_URI', - 'OAUTH_TOKEN_URI', - 'SERVER_URL', - 'SES_CONFIG_SET', - 'SMTP_URL', - 'SUPPORTED_LOCALES' -] - -const optionalEnvVars = [ - 'FX_REMOTE_SETTINGS_WRITER_PASS', - 'FX_REMOTE_SETTINGS_WRITER_SERVER', - 'FX_REMOTE_SETTINGS_WRITER_USER', - 'HIBP_BREACH_DOMAIN_BLOCKLIST', - 'PREMIUM_PRODUCT_ID', - 'PG_HOST', - 'NEXTAUTH_REDIRECT_URL' -] - -/** @type {Record} */ -const AppConstants = { } - -if (!process.env.SERVER_URL && (process.env.APP_ENV) === 'heroku') { - process.env.SERVER_URL = `https://${process.env.HEROKU_APP_NAME}.herokuapp.com` -} - -for (const v of requiredEnvVars) { - const value = process.env[v] - if (value === undefined) { - console.warn(`Required environment variable was not set: ${v}`) - } else { - AppConstants[v] = value - } -} - -optionalEnvVars.forEach(key => { - const value = process.env[key] - if (value) AppConstants[key] = value -}) - -export default AppConstants.NODE_ENV === 'test' - ? AppConstants - : Object.freeze(AppConstants) diff --git a/src/db/tables/subscribers.ts b/src/db/tables/subscribers.ts index fe55910427e..5e52eeac09c 100644 --- a/src/db/tables/subscribers.ts +++ b/src/db/tables/subscribers.ts @@ -5,12 +5,14 @@ import type { Profile } from "next-auth"; import type { EmailAddressRow, SubscriberRow } from "knex/types/tables"; import createDbConnection from "../connect"; -import AppConstants from "../../appConstants.js"; import { SerializedSubscriber } from "../../next-auth.js"; import { getFeatureFlagData } from "./featureFlags"; +import { getEnvVarsOrThrow } from "../../envVars"; const knex = createDbConnection(); -const { DELETE_UNVERIFIED_SUBSCRIBERS_TIMER } = AppConstants; +const { DELETE_UNVERIFIED_SUBSCRIBERS_TIMER } = getEnvVarsOrThrow([ + "DELETE_UNVERIFIED_SUBSCRIBERS_TIMER", +]); const MONITOR_PREMIUM_CAPABILITY = "monitor"; // Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy diff --git a/src/envVars.ts b/src/envVars.ts new file mode 100644 index 00000000000..f4c06193d4a --- /dev/null +++ b/src/envVars.ts @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +if ( + typeof process.env.NEXT_RUNTIME === "undefined" && + typeof process.env.STORYBOOK === "undefined" +) { + // Next.js already loads env vars by itself, and dotenv-flow will throw an + // error if loaded in that context (about `fs` not existing), so only load + // it if we're not running in a Next.js-context (e.g. cron jobs): + await import("dotenv-flow/config"); +} + +export function getEnvVarsOrThrow( + envVars: EnvVarNames[], +): Record { + const envVarsRecord: Record = {} as never; + for (const varName of envVars) { + const value = process.env[varName]; + if (typeof value !== "string") { + throw new Error( + `Required environment variable was not set: [${varName}].`, + ); + } + envVarsRecord[varName] = value; + } + return envVarsRecord; +} diff --git a/src/scripts/cronjobs/updateBreachesInRemoteSettings.ts b/src/scripts/cronjobs/updateBreachesInRemoteSettings.ts index c0d9c2c0a97..d020c76b891 100644 --- a/src/scripts/cronjobs/updateBreachesInRemoteSettings.ts +++ b/src/scripts/cronjobs/updateBreachesInRemoteSettings.ts @@ -33,7 +33,6 @@ * */ -import AppConstants from "../../appConstants"; import * as HIBP from "../../utils/hibp"; type RemoteSettingsBreach = Pick< @@ -41,11 +40,29 @@ type RemoteSettingsBreach = Pick< "Name" | "Domain" | "BreachDate" | "PwnCount" | "AddedDate" | "DataClasses" >; +const FX_REMOTE_SETTINGS_WRITER_USER = + process.env.FX_REMOTE_SETTINGS_WRITER_USER; +const FX_REMOTE_SETTINGS_WRITER_PASS = + process.env.FX_REMOTE_SETTINGS_WRITER_PASS; +const FX_REMOTE_SETTINGS_WRITER_SERVER = + process.env.FX_REMOTE_SETTINGS_WRITER_SERVER; + +if ( + !FX_REMOTE_SETTINGS_WRITER_USER || + !FX_REMOTE_SETTINGS_WRITER_PASS || + !FX_REMOTE_SETTINGS_WRITER_SERVER +) { + console.error( + "updatebreaches requires FX_REMOTE_SETTINGS_WRITER_SERVER, FX_REMOTE_SETTINGS_WRITER_USER, FX_REMOTE_SETTINGS_WRITER_PASS.", + ); + process.exit(1); +} + const BREACHES_COLLECTION = "fxmonitor-breaches"; -const FX_RS_COLLECTION = `${AppConstants.FX_REMOTE_SETTINGS_WRITER_SERVER}/buckets/main-workspace/collections/${BREACHES_COLLECTION}`; +const FX_RS_COLLECTION = `${FX_REMOTE_SETTINGS_WRITER_SERVER}/buckets/main-workspace/collections/${BREACHES_COLLECTION}`; const FX_RS_RECORDS = `${FX_RS_COLLECTION}/records`; -const FX_RS_WRITER_USER = AppConstants.FX_REMOTE_SETTINGS_WRITER_USER; -const FX_RS_WRITER_PASS = AppConstants.FX_REMOTE_SETTINGS_WRITER_PASS; +const FX_RS_WRITER_USER = FX_REMOTE_SETTINGS_WRITER_USER; +const FX_RS_WRITER_PASS = FX_REMOTE_SETTINGS_WRITER_PASS; async function whichBreachesAreNotInRemoteSettingsYet( breaches: HIBP.HibpGetBreachesResponse, @@ -90,17 +107,6 @@ async function requestReviewOnBreachesCollection() { return response.json(); } -if ( - !AppConstants.FX_REMOTE_SETTINGS_WRITER_USER || - !AppConstants.FX_REMOTE_SETTINGS_WRITER_PASS || - !AppConstants.FX_REMOTE_SETTINGS_WRITER_SERVER -) { - console.error( - "updatebreaches requires FX_REMOTE_SETTINGS_WRITER_SERVER, FX_REMOTE_SETTINGS_WRITER_USER, FX_REMOTE_SETTINGS_WRITER_PASS.", - ); - process.exit(1); -} - (async () => { const allHibpBreaches = await HIBP.fetchHibpBreaches(); const verifiedSiteBreaches = allHibpBreaches.filter((breach) => { diff --git a/src/utils/dockerflow.ts b/src/utils/dockerflow.ts index c33c30bc6e6..549d1e058ff 100644 --- a/src/utils/dockerflow.ts +++ b/src/utils/dockerflow.ts @@ -6,7 +6,6 @@ import fs from "fs"; import path from "path"; -import AppConstants from "../appConstants"; import packageJson from "../../package.json"; export type VersionData = { @@ -23,7 +22,7 @@ if (!fs.existsSync(versionJsonPath)) { const versionJson = { source: packageJson.homepage, version: packageJson.version, - NODE_ENV: AppConstants.NODE_ENV, + NODE_ENV: process.env.NODE_ENV, }; fs.writeFileSync( @@ -33,7 +32,7 @@ if (!fs.existsSync(versionJsonPath)) { } export function vers(): VersionData { - if (AppConstants.APP_ENV === "heroku") { + if (process.env.APP_ENV === "heroku") { /* eslint-disable no-process-env */ return { commit: process.env.HEROKU_SLUG_COMMIT!, diff --git a/src/utils/email.ts b/src/utils/email.ts index 29337f36e17..cd6769d213a 100644 --- a/src/utils/email.ts +++ b/src/utils/email.ts @@ -4,14 +4,16 @@ import { createTransport, Transporter } from "nodemailer"; -import AppConstants from "../appConstants.js"; import { SentMessageInfo } from "nodemailer/lib/smtp-transport/index.js"; +import { getEnvVarsOrThrow } from "../envVars"; // The SMTP transport object. This is initialized to a nodemailer transport // object while reading SMTP credentials, or to a dummy function in debug mode. let gTransporter: Transporter; -async function initEmail(smtpUrl = AppConstants.SMTP_URL) { +const envVars = getEnvVarsOrThrow(["SMTP_URL", "EMAIL_FROM", "SES_CONFIG_SET"]); + +async function initEmail(smtpUrl = envVars.SMTP_URL) { // Allow a debug mode that will log JSON instead of sending emails. if (!smtpUrl) { console.info("smtpUrl-empty", { @@ -42,14 +44,14 @@ async function sendEmail( throw new Error("SMTP transport not initialized"); } - const emailFrom = AppConstants.EMAIL_FROM; + const emailFrom = envVars.EMAIL_FROM; const mailOptions = { from: emailFrom, to: recipient, subject, html, headers: { - "x-ses-configuration-set": AppConstants.SES_CONFIG_SET, + "x-ses-configuration-set": envVars.SES_CONFIG_SET, }, }; diff --git a/src/utils/fxa.ts b/src/utils/fxa.ts index da7d8902c57..43f0248a7bb 100644 --- a/src/utils/fxa.ts +++ b/src/utils/fxa.ts @@ -5,7 +5,13 @@ import crypto from "crypto"; import { URL } from "url"; -import AppConstants from "../appConstants.js"; +import { getEnvVarsOrThrow } from "../envVars"; +const envVars = getEnvVarsOrThrow([ + "OAUTH_CLIENT_ID", + "OAUTH_CLIENT_SECRET", + "OAUTH_TOKEN_URI", + "OAUTH_ACCOUNT_URI", +]); /** * @see https://mozilla.github.io/ecosystem-platform/api#tag/Oauth/operation/postOauthDestroy @@ -30,11 +36,11 @@ async function destroyOAuthToken( ) { const tokenBody: FxaPostOauthDestroyRequestBody = { ...tokenData, - client_id: AppConstants.OAUTH_CLIENT_ID, - client_secret: AppConstants.OAUTH_CLIENT_SECRET, + client_id: envVars.OAUTH_CLIENT_ID, + client_secret: envVars.OAUTH_CLIENT_SECRET, }; - const fxaTokenOrigin = new URL(AppConstants.OAUTH_TOKEN_URI).origin; + const fxaTokenOrigin = new URL(envVars.OAUTH_TOKEN_URI).origin; const tokenUrl = `${fxaTokenOrigin}/v1/oauth/destroy`; const tokenOptions = { method: "POST", @@ -132,10 +138,10 @@ type FxaPostOauthTokenResponseSuccessRefreshToken = { async function refreshOAuthTokens( refreshToken: string, ): Promise { - const subscriptionIdUrl = `${AppConstants.OAUTH_ACCOUNT_URI}/oauth/token`; + const subscriptionIdUrl = `${envVars.OAUTH_ACCOUNT_URI}/oauth/token`; const body: FxaPostOauthTokenRequestBody = { - client_id: AppConstants.OAUTH_CLIENT_ID, - client_secret: AppConstants.OAUTH_CLIENT_SECRET, + client_id: envVars.OAUTH_CLIENT_ID, + client_secret: envVars.OAUTH_CLIENT_SECRET, grant_type: "refresh_token", refresh_token: refreshToken, ttl: 604800, // request 7 days ttl @@ -175,7 +181,7 @@ type FxaGetOauthSubscribptionsActiveResponseSuccess = Array<{ async function getSubscriptions( bearerToken: string, ): Promise { - const subscriptionIdUrl = `${AppConstants.OAUTH_ACCOUNT_URI}/oauth/subscriptions/active`; + const subscriptionIdUrl = `${envVars.OAUTH_ACCOUNT_URI}/oauth/subscriptions/active`; try { const response = await fetch(subscriptionIdUrl, { headers: { @@ -221,7 +227,7 @@ type FxaGetOauthMozillaSubscribptionsCustomerBillingAndSubscriptionsResponseSucc async function getBillingAndSubscriptions( bearerToken: string, ): Promise { - const subscriptionIdUrl = `${AppConstants.OAUTH_ACCOUNT_URI}/oauth/mozilla-subscriptions/customer/billing-and-subscriptions`; + const subscriptionIdUrl = `${envVars.OAUTH_ACCOUNT_URI}/oauth/mozilla-subscriptions/customer/billing-and-subscriptions`; try { const response = await fetch(subscriptionIdUrl, { @@ -253,13 +259,13 @@ async function deleteSubscription(bearerToken: string): Promise { if ( sub && sub.productId && - sub.productId === AppConstants.PREMIUM_PRODUCT_ID + sub.productId === process.env.PREMIUM_PRODUCT_ID ) { subscriptionId = sub.subscriptionId; } } if (subscriptionId) { - const deleteUrl = `${AppConstants.OAUTH_ACCOUNT_URI}/oauth/subscriptions/active/${subscriptionId}`; + const deleteUrl = `${envVars.OAUTH_ACCOUNT_URI}/oauth/subscriptions/active/${subscriptionId}`; const response = await fetch(deleteUrl, { method: "DELETE", headers: { @@ -294,13 +300,13 @@ async function applyCoupon( if ( sub && sub.productId && - sub.productId === AppConstants.PREMIUM_PRODUCT_ID + sub.productId === process.env.PREMIUM_PRODUCT_ID ) { subscriptionId = sub.subscriptionId; } } if (subscriptionId) { - const applyCouponUrl = `${AppConstants.OAUTH_ACCOUNT_URI}/oauth/subscriptions/coupon/apply`; + const applyCouponUrl = `${envVars.OAUTH_ACCOUNT_URI}/oauth/subscriptions/coupon/apply`; const response = await fetch(applyCouponUrl, { method: "PUT", headers: { diff --git a/src/utils/hibp.ts b/src/utils/hibp.ts index 0dce9148ce5..4e96effc734 100644 --- a/src/utils/hibp.ts +++ b/src/utils/hibp.ts @@ -2,7 +2,6 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import AppConstants from "../appConstants.js"; import { getAllBreaches, knex } from "../db/tables/breaches"; import { isUsingMockHIBPEndpoint } from "../app/functions/universal/mock.ts"; import { BreachRow, EmailAddressRow, SubscriberRow } from "knex/types/tables"; @@ -14,13 +13,20 @@ import { getQaToggleRow, } from "../db/tables/qa_customs.ts"; import { redisClient, REDIS_ALL_BREACHES_KEY } from "../db/redis/client.ts"; +import { getEnvVarsOrThrow } from "../envVars.ts"; const { HIBP_THROTTLE_MAX_TRIES, HIBP_THROTTLE_DELAY, HIBP_API_ROOT, HIBP_KANON_API_ROOT, HIBP_KANON_API_TOKEN, -} = AppConstants; +} = getEnvVarsOrThrow([ + "HIBP_THROTTLE_MAX_TRIES", + "HIBP_THROTTLE_DELAY", + "HIBP_API_ROOT", + "HIBP_KANON_API_ROOT", + "HIBP_KANON_API_TOKEN", +]); // TODO: fix hardcode const HIBP_USER_AGENT = "monitor/1.0.0"; @@ -73,8 +79,10 @@ async function _throttledFetch( } else { tryCount++; await new Promise((resolve) => - // @ts-ignore HIBP_THROTTLE_DELAY should be defined - setTimeout(resolve, HIBP_THROTTLE_DELAY * tryCount), + setTimeout( + resolve, + Number.parseInt(HIBP_THROTTLE_DELAY, 10) * tryCount, + ), ); return await _throttledFetch(url, reqOptions, tryCount); }