/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/no-explicit-any */ import * as dotenv from 'dotenv' import os from 'os' export interface BackendEnv { pg: { host: string port: number userName: string password: string dbName: string pool: { max: number } } server: { jwtSecret: string ssoJwtSecret: string gateway_url: string apiEnv: string instanceId: string trustProxy: boolean internalApiUrl: string } client: { url: string } google: { auth: { iosClientId: string androidClientId: string clientId: string secret: string } } segment: { writeKey: string } intercom: { token: string secretKey: string } sentry: { dsn: string } jaeger: { host: string } imageProxy: { url: string secretKey: string } twitter: { token: string } dev: { isLocal: boolean } queue: { location: string name: string contentFetchUrl: string contentFetchGCFUrl: string reminderTaskHandlerUrl: string integrationTaskHandlerUrl: string textToSpeechTaskHandlerUrl: string recommendationTaskHandlerUrl: string thumbnailTaskHandlerUrl: string rssFeedTaskHandlerUrl: string integrationExporterUrl: string integrationImporterUrl: string importerMetricsUrl: string } fileUpload: { gcsUploadBucket: string gcsUploadSAKeyFilePath: string gcsUploadPrivateBucket: string } sender: { message: string feedback: string general: string } sendgrid: { confirmationTemplateId: string reminderTemplateId: string resetPasswordTemplateId: string installationTemplateId: string verificationTemplateId: string } readwise: { apiUrl: string } gcp: { location: string } pocket: { consumerKey: string } subscription: { feed: { max: number } } redis: { url?: string cert?: string } } const nullableEnvVars = [ 'INTERCOM_TOKEN', 'INTERCOM_SECRET_KEY', 'GAE_INSTANCE', 'SENTRY_DSN', 'SENTRY_AUTH_TOKEN', 'SENTRY_ORG', 'SENTRY_PROJECT', 'JAEGER_HOST', 'IMAGE_PROXY_URL', 'IMAGE_PROXY_SECRET', 'SAMPLE_METRICS_LOCALLY', 'PUPPETEER_QUEUE_LOCATION', 'PUPPETEER_QUEUE_NAME', 'CONTENT_FETCH_URL', 'CONTENT_FETCH_GCF_URL', 'GCS_UPLOAD_SA_KEY_FILE_PATH', 'GAUTH_IOS_CLIENT_ID', 'GAUTH_ANDROID_CLIENT_ID', 'GAUTH_CLIENT_ID', 'GAUTH_SECRET', 'SEGMENT_WRITE_KEY', 'TWITTER_BEARER_TOKEN', 'GCS_UPLOAD_PRIVATE_BUCKET', 'SENDER_MESSAGE', 'SENDER_FEEDBACK', 'SENDER_GENERAL', 'SENDGRID_CONFIRMATION_TEMPLATE_ID', 'SENDGRID_REMINDER_TEMPLATE_ID', 'SENDGRID_RESET_PASSWORD_TEMPLATE_ID', 'SENDGRID_INSTALLATION_TEMPLATE_ID', 'READWISE_API_URL', 'INTEGRATION_TASK_HANDLER_URL', 'TEXT_TO_SPEECH_TASK_HANDLER_URL', 'GCP_LOCATION', 'RECOMMENDATION_TASK_HANDLER_URL', 'POCKET_CONSUMER_KEY', 'THUMBNAIL_TASK_HANDLER_URL', 'RSS_FEED_TASK_HANDLER_URL', 'SENDGRID_VERIFICATION_TEMPLATE_ID', 'REMINDER_TASK_HANDLER_URL', 'TRUST_PROXY', 'INTEGRATION_EXPORTER_URL', 'INTEGRATION_IMPORTER_URL', 'SUBSCRIPTION_FEED_MAX', 'REDIS_URL', 'REDIS_CERT', 'IMPORTER_METRICS_COLLECTOR_URL', 'INTERNAL_API_URL', ] // Allow some vars to be null/empty /* If not in GAE and Prod/QA/Demo env (f.e. on localhost/dev env), allow following env vars to be null */ if (process.env.API_ENV == 'local') { nullableEnvVars.push(...['GCS_UPLOAD_BUCKET']) } const envParser = (env: { [key: string]: string | undefined }) => (varName: string): string => { const value = env[varName] if (typeof value === 'string' && value) { return value } else if (nullableEnvVars.includes(varName)) { return '' } throw new Error( `Missing ${varName} with a non-empty value in process environment` ) } interface Dict { [key: string]: T | undefined } export function getEnv(): BackendEnv { // Dotenv parses env file merging into proces.env which is then read into custom struct here. dotenv.config() const parse = envParser(process.env) const pg = { host: parse('PG_HOST'), port: parseInt(parse('PG_PORT'), 10), userName: parse('PG_USER'), password: parse('PG_PASSWORD'), dbName: parse('PG_DB'), pool: { max: parseInt(parse('PG_POOL_MAX'), 10), }, } const server = { jwtSecret: parse('JWT_SECRET'), ssoJwtSecret: parse('SSO_JWT_SECRET'), gateway_url: parse('GATEWAY_URL'), apiEnv: parse('API_ENV'), instanceId: parse('GAE_INSTANCE') || `x${os.userInfo().username}_${os.hostname()}`, trustProxy: parse('TRUST_PROXY') === 'true', internalApiUrl: parse('INTERNAL_API_URL'), } const client = { url: parse('CLIENT_URL'), } const google = { auth: { iosClientId: parse('GAUTH_IOS_CLIENT_ID'), androidClientId: parse('GAUTH_ANDROID_CLIENT_ID'), clientId: parse('GAUTH_CLIENT_ID'), secret: parse('GAUTH_SECRET'), }, } const segment = { writeKey: parse('SEGMENT_WRITE_KEY'), } const intercom = { token: parse('INTERCOM_TOKEN'), secretKey: parse('INTERCOM_SECRET_KEY'), } const sentry = { dsn: parse('SENTRY_DSN'), } const jaeger = { host: parse('JAEGER_HOST'), } const dev = { isLocal: parse('API_ENV') == 'local', } const queue = { location: parse('PUPPETEER_QUEUE_LOCATION'), name: parse('PUPPETEER_QUEUE_NAME'), contentFetchUrl: parse('CONTENT_FETCH_URL'), contentFetchGCFUrl: parse('CONTENT_FETCH_GCF_URL'), reminderTaskHandlerUrl: parse('REMINDER_TASK_HANDLER_URL'), integrationTaskHandlerUrl: parse('INTEGRATION_TASK_HANDLER_URL'), textToSpeechTaskHandlerUrl: parse('TEXT_TO_SPEECH_TASK_HANDLER_URL'), recommendationTaskHandlerUrl: parse('RECOMMENDATION_TASK_HANDLER_URL'), thumbnailTaskHandlerUrl: parse('THUMBNAIL_TASK_HANDLER_URL'), rssFeedTaskHandlerUrl: parse('RSS_FEED_TASK_HANDLER_URL'), integrationExporterUrl: parse('INTEGRATION_EXPORTER_URL'), integrationImporterUrl: parse('INTEGRATION_IMPORTER_URL'), importerMetricsUrl: parse('IMPORTER_METRICS_COLLECTOR_URL'), } const imageProxy = { url: parse('IMAGE_PROXY_URL'), secretKey: parse('IMAGE_PROXY_SECRET'), } const twitter = { token: parse('TWITTER_BEARER_TOKEN'), } const fileUpload = { gcsUploadBucket: parse('GCS_UPLOAD_BUCKET'), gcsUploadSAKeyFilePath: parse('GCS_UPLOAD_SA_KEY_FILE_PATH'), gcsUploadPrivateBucket: parse('GCS_UPLOAD_PRIVATE_BUCKET'), } const sender = { message: parse('SENDER_MESSAGE'), feedback: parse('SENDER_FEEDBACK'), general: parse('SENDER_GENERAL'), } const sendgrid = { confirmationTemplateId: parse('SENDGRID_CONFIRMATION_TEMPLATE_ID'), reminderTemplateId: parse('SENDGRID_REMINDER_TEMPLATE_ID'), resetPasswordTemplateId: parse('SENDGRID_RESET_PASSWORD_TEMPLATE_ID'), installationTemplateId: parse('SENDGRID_INSTALLATION_TEMPLATE_ID'), verificationTemplateId: parse('SENDGRID_VERIFICATION_TEMPLATE_ID'), } const readwise = { apiUrl: parse('READWISE_API_URL'), } const gcp = { location: parse('GCP_LOCATION'), } const pocket = { consumerKey: parse('POCKET_CONSUMER_KEY'), } const subscription = { feed: { max: parse('SUBSCRIPTION_FEED_MAX') ? parseInt(parse('SUBSCRIPTION_FEED_MAX'), 10) : 256, // default to 256 }, } const redis = { url: parse('REDIS_URL'), cert: parse('REDIS_CERT')?.replace(/\\n/g, '\n'), // replace \n with new line } return { pg, client, server, google, segment, intercom, sentry, jaeger, imageProxy, twitter, dev, fileUpload, queue, sender, sendgrid, readwise, gcp, pocket, subscription, redis, } } export type Merge< Target extends Record, Part extends Record > = Omit & Part /** * Make all properties in T optional * This is similar to TS's Partial type, but it also allows null */ export type Partialize = { [P in keyof T]?: T[P] | null } export function exclude( a: A, b: B ): readonly Exclude[] { return a.filter((x) => b.includes(x)) as any } export type PickTuple = Pick