* fix: Library Header layout shift * Bump Github Actions versions. * Self-Hosting Changes * Fix Minio Environment Variable * Just make pdfs successful, due to lack of PDFHandler * Fix issue where flag was set wrong * Added an NGINX Example file * Add some documentation for self-hosting via Docker Compose * Make some adjustments to Puppeteer due to failing sites. * adjust timings * Add start of Mail Service * Fix Docker Files * More email service stuff * Add Guide to use Zapier for Email-Importing. * Ensure that if no env is provided it uses the old email settings * Add some instructions for self-hosted email * Add SNS Endpoints for Mail Watcher * Add steps and functionality for using SES and SNS for email * Uncomment a few jobs. * Added option for Firefox for parser. Was having issues with Chromium on Docker. * Add missing space. Co-authored-by: Russ Taylor <729694+russtaylor@users.noreply.github.com> * Fix some wording on the Guide * update browser extension to handle self-hosted instances * add slight documentation to options page * Fix MV * Do raw handlers for Medium * Fix images in Medium * Update self-hosting/GUIDE.md Co-authored-by: Mike Baker <1426795+mbaker3@users.noreply.github.com> * Update Guide with other variables * Add The Verge to JS-less handlers * Update regex and image-proxy * Update self-hosting/nginx/nginx.conf Co-authored-by: Mike Baker <1426795+mbaker3@users.noreply.github.com> * Update regex and image-proxy * Update self-hosting/docker-compose/docker-compose.yml Co-authored-by: Mike Baker <1426795+mbaker3@users.noreply.github.com> * Fix Minio for Export * Merge to main * Update GUIDE with newer NGINX * Update nginx config to include api/save route * Enable Native PDF View for PDFS * Enable Native PDF View for PDFS * feat:lover packages test * feat:working build * feat:alpine build * docs:api dockerfile docs * Write a PDF.js wrapper to replace pspdfkit * Revert changes for replication, set settings to have default mode * build folder got removed due to gitignore on pdf * Add Box shadow to pdf pages * Add Toggle for Progress in PDFS, enabled native viewer toggle * Update node version to LTS * Update node version to LTS * Fix Linting issues * Fix Linting issues * Make env variable nullable * Add touchend listener for mobile * Make changes to PDF for mobile * fix(android): change serverUrl to selfhosted first * feat:2 stage alpine content fetch * feat:separated start script * fix:changed to node 22 * Add back youtube functionality and add guide * trigger build * Fix cache issue on YouTube * Allow empty AWS_S3_ENDPOINT * Allow empty AWS_S3_ENDPOINT * Add GCHR for all images * Add GCHR For self hosting. * Add GCHR For self hosting. * Test prebuilt. * Test prebuilt * Test prebuilt... * Fix web image * Remove Web Image (For now) * Move docker-compose to images * Move docker-compose files to correct locations * Remove the need for ARGS * Update packages, and Typescript versions * Fix * Fix issues with build on Web * Correct push * Fix Linting issues * Fix Trace import * Add missing types * Fix Tasks * Add information into guide about self-build * Fix issues with PDF Viewer --------- Co-authored-by: keumky2 <keumky2@woowahan.com> Co-authored-by: William Theaker <wtheaker@nvidia.com> Co-authored-by: Russ Taylor <729694+russtaylor@users.noreply.github.com> Co-authored-by: David Adams <david@dadams2.com> Co-authored-by: Mike Baker <1426795+mbaker3@users.noreply.github.com> Co-authored-by: m1xxos <66390094+m1xxos@users.noreply.github.com> Co-authored-by: Adil <mr.adil777@gmail.com>
442 lines
11 KiB
TypeScript
Executable File
442 lines
11 KiB
TypeScript
Executable File
/* 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'
|
|
|
|
interface redisConfig {
|
|
url?: string
|
|
cert?: string
|
|
}
|
|
|
|
export interface BackendEnv {
|
|
pg: {
|
|
host: string
|
|
port: number
|
|
userName: string
|
|
password: string
|
|
dbName: string
|
|
pool: {
|
|
max: number
|
|
}
|
|
replication: boolean
|
|
replica: {
|
|
host: string
|
|
port: number
|
|
userName: string
|
|
password: string
|
|
dbName: string
|
|
}
|
|
}
|
|
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
|
|
}
|
|
}
|
|
posthog: {
|
|
apiKey: string
|
|
}
|
|
intercom: {
|
|
token: string
|
|
secretKey: string
|
|
webSecret: string
|
|
iosSecret: string
|
|
androidSecret: string
|
|
}
|
|
sentry: {
|
|
dsn: string
|
|
}
|
|
jaeger: {
|
|
host: string
|
|
}
|
|
imageProxy: {
|
|
url: string
|
|
secretKey: string
|
|
}
|
|
twitter: {
|
|
token: string
|
|
}
|
|
dev: {
|
|
isLocal: boolean
|
|
autoVerify: boolean
|
|
}
|
|
queue: {
|
|
location: string
|
|
name: string
|
|
contentFetchUrl: string
|
|
contentFetchGCFUrl: string
|
|
reminderTaskHandlerUrl: string
|
|
integrationTaskHandlerUrl: string
|
|
textToSpeechTaskHandlerUrl: string
|
|
recommendationTaskHandlerUrl: string
|
|
thumbnailTaskHandlerUrl: string
|
|
integrationExporterUrl: string
|
|
integrationImporterUrl: string
|
|
importerMetricsUrl: string
|
|
exportTaskHandlerUrl: string
|
|
}
|
|
fileUpload: {
|
|
gcsUploadBucket: string
|
|
gcsUploadSAKeyFilePath: string
|
|
gcsUploadPrivateBucket: string
|
|
dailyUploadLimit: number
|
|
useLocalStorage: boolean
|
|
localMinioUrl: string
|
|
internalMinioUrl: string
|
|
}
|
|
email: {
|
|
domain: 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: {
|
|
mq: redisConfig
|
|
cache: redisConfig
|
|
}
|
|
notion: {
|
|
clientId: string
|
|
clientSecret: string
|
|
authUrl: string
|
|
}
|
|
score: {
|
|
apiUrl: 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',
|
|
'POSTHOG_API_KEY',
|
|
'TWITTER_BEARER_TOKEN',
|
|
'GCS_UPLOAD_PRIVATE_BUCKET',
|
|
'GCS_UPLOAD_DAILY_LIMIT',
|
|
'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',
|
|
'SENDGRID_VERIFICATION_TEMPLATE_ID',
|
|
'REMINDER_TASK_HANDLER_URL',
|
|
'TRUST_PROXY',
|
|
'INTEGRATION_EXPORTER_URL',
|
|
'INTEGRATION_IMPORTER_URL',
|
|
'SUBSCRIPTION_FEED_MAX',
|
|
'REDIS_URL',
|
|
'REDIS_CERT',
|
|
'MQ_REDIS_URL',
|
|
'MQ_REDIS_CERT',
|
|
'IMPORTER_METRICS_COLLECTOR_URL',
|
|
'INTERNAL_API_URL',
|
|
'NOTION_CLIENT_ID',
|
|
'NOTION_CLIENT_SECRET',
|
|
'NOTION_AUTH_URL',
|
|
'SCORE_API_URL',
|
|
'PG_REPLICATION',
|
|
'PG_REPLICA_HOST',
|
|
'PG_REPLICA_PORT',
|
|
'PG_REPLICA_USER',
|
|
'PG_REPLICA_PASSWORD',
|
|
'PG_REPLICA_DB',
|
|
'AUTO_VERIFY',
|
|
'INTERCOM_WEB_SECRET',
|
|
'INTERCOM_IOS_SECRET',
|
|
'INTERCOM_ANDROID_SECRET',
|
|
'EXPORT_TASK_HANDLER_URL',
|
|
'LOCAL_MINIO_URL',
|
|
'GCS_USE_LOCAL_HOST',
|
|
'LOCAL_EMAIL_DOMAIN',
|
|
'AWS_S3_ENDPOINT_URL',
|
|
] // Allow some vars to be null/empty
|
|
|
|
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<T> {
|
|
[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()
|
|
|
|
/* 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 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),
|
|
},
|
|
|
|
replication: parse('PG_REPLICATION') === 'true',
|
|
replica: {
|
|
host: parse('PG_REPLICA_HOST'),
|
|
port: parseInt(parse('PG_REPLICA_PORT'), 10),
|
|
userName: parse('PG_REPLICA_USER'),
|
|
password: parse('PG_REPLICA_PASSWORD'),
|
|
dbName: parse('PG_REPLICA_DB'),
|
|
},
|
|
}
|
|
const email = {
|
|
domain: parse('LOCAL_EMAIL_DOMAIN'),
|
|
}
|
|
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 posthog = {
|
|
apiKey: parse('POSTHOG_API_KEY'),
|
|
}
|
|
const intercom = {
|
|
token: parse('INTERCOM_TOKEN'),
|
|
secretKey: parse('INTERCOM_SECRET_KEY'),
|
|
webSecret: parse('INTERCOM_WEB_SECRET'),
|
|
iosSecret: parse('INTERCOM_IOS_SECRET'),
|
|
androidSecret: parse('INTERCOM_ANDROID_SECRET'),
|
|
}
|
|
const sentry = {
|
|
dsn: parse('SENTRY_DSN'),
|
|
}
|
|
const jaeger = {
|
|
host: parse('JAEGER_HOST'),
|
|
}
|
|
const dev = {
|
|
isLocal: parse('API_ENV') == 'local',
|
|
autoVerify: parse('AUTO_VERIFY') === 'true',
|
|
}
|
|
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'),
|
|
integrationExporterUrl: parse('INTEGRATION_EXPORTER_URL'),
|
|
integrationImporterUrl: parse('INTEGRATION_IMPORTER_URL'),
|
|
importerMetricsUrl: parse('IMPORTER_METRICS_COLLECTOR_URL'),
|
|
exportTaskHandlerUrl: parse('EXPORT_TASK_HANDLER_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'),
|
|
dailyUploadLimit: parse('GCS_UPLOAD_DAILY_LIMIT')
|
|
? parseInt(parse('GCS_UPLOAD_DAILY_LIMIT'), 10)
|
|
: 5, // default to 5
|
|
useLocalStorage: parse('GCS_USE_LOCAL_HOST') == 'true',
|
|
localMinioUrl: parse('LOCAL_MINIO_URL'),
|
|
internalMinioUrl: parse('AWS_S3_ENDPOINT_URL'),
|
|
}
|
|
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 = {
|
|
mq: {
|
|
url: parse('MQ_REDIS_URL'),
|
|
cert: parse('MQ_REDIS_CERT')?.replace(/\\n/g, '\n'), // replace \n with new line
|
|
},
|
|
cache: {
|
|
url: parse('REDIS_URL'),
|
|
cert: parse('REDIS_CERT')?.replace(/\\n/g, '\n'), // replace \n with new line
|
|
},
|
|
}
|
|
const notion = {
|
|
clientId: parse('NOTION_CLIENT_ID'),
|
|
clientSecret: parse('NOTION_CLIENT_SECRET'),
|
|
authUrl: parse('NOTION_AUTH_URL'),
|
|
}
|
|
const score = {
|
|
apiUrl: parse('SCORE_API_URL') || 'http://digest-score/batch',
|
|
}
|
|
|
|
return {
|
|
pg,
|
|
client,
|
|
email,
|
|
server,
|
|
google,
|
|
posthog,
|
|
intercom,
|
|
sentry,
|
|
jaeger,
|
|
imageProxy,
|
|
twitter,
|
|
dev,
|
|
fileUpload,
|
|
queue,
|
|
sender,
|
|
sendgrid,
|
|
readwise,
|
|
gcp,
|
|
pocket,
|
|
subscription,
|
|
redis,
|
|
notion,
|
|
score,
|
|
}
|
|
}
|
|
|
|
export type Merge<
|
|
Target extends Record<string, any>,
|
|
Part extends Record<string, any>
|
|
> = Omit<Target, keyof Part> & Part
|
|
|
|
/**
|
|
* Make all properties in T optional
|
|
* This is similar to TS's Partial type, but it also allows null
|
|
*/
|
|
export type Partialize<T> = {
|
|
[P in keyof T]?: T[P] | null
|
|
}
|
|
|
|
export function exclude<A extends readonly any[], B extends readonly any[]>(
|
|
a: A,
|
|
b: B
|
|
): readonly Exclude<A[number], B[number]>[] {
|
|
return a.filter((x) => b.includes(x)) as any
|
|
}
|
|
|
|
export type PickTuple<A, B extends readonly any[]> = Pick<A, B[number]>
|