// Copyright Abridged, Inc. 2021,2024. All Rights Reserved. // Node module: @collabland/common // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT import lo from 'lodash'; import Module, {createRequire} from 'module'; import path from 'path'; import {fileURLToPath} from 'url'; import type {AnyType} from '../types.js'; const {template} = lo; import nodeEvents from 'events'; nodeEvents.defaultMaxListeners = 32; declare global { // eslint-disable-next-line no-var var collabLandSecrets: Record; } /** * Enumeration of environment types */ export enum EnvType { /** * Production */ PROD = 'production', /** * Staging (preparing for production) */ STAGING = 'staging', /** * QA - ready for QA */ QA = 'qa', /** * Development mode */ DEV = 'dev', /** * For tests */ TEST = 'test', } /** * Enumeration of resource types */ export enum ResourceType { /** * DynamoDB table for APIs */ DynamoDBAPITable = 'DynamoDBAPITable', /** * DynamoDB table for CollabLand entities */ DynamoDBTable = 'DynamoDBTable', /** * S3 bucket name */ S3Bucket = 'S3Bucket', /** * SQS queue for jobs */ SQSJobsQueue = 'SQSJobsQueue', SQSJobsJoinQueue = 'SQSJobsJoinQueue', SQSJobsEventsQueue = 'SQSJobsEventsQueue', /** * SNS topic for jobs */ SNSJobsTopic = 'SNSJobsTopic', /** * KMS CMK alias */ KMSKeyAlias = 'KMSKeyAlias', /** * Elastic BeanStalk API server */ APIServer = 'APIServer', /** * Secret name */ SecretName = 'SecretName', /** * Redpacket claims queue name */ SQSRedPacketClaimsQueue = 'SQSRedPacketClaimsQueue', /** * Token claims queue name */ SQSTokenClaimsQueue = 'SQSTokenClaimsQueue', /** * Token simple claims queue name */ SQSTokenSimpleClaimsQueue = 'SQSTokenSimpleClaimsQueue', APIServerURL = 'APIServerURL', LoginURL = 'LoginURL', CommandCenterURL = 'CommandCenterURL', WalletConnectionURL = 'WalletConnectionURL', GCPBigqueryDatabaseID = 'GCPBigqueryDatabaseID', GCPBigqueryTableID = 'GCPBigqueryTableID', GCPBigqueryMixPanelTableID = 'GCPBigqueryMixPanelTableID', GCPBigqueryTelefrensTableID = 'GCPBigqueryTelefrensTableID', GCPBigqueryTipLitErrorsTableID = 'GCPBigqueryTipLitErrorsTableID', } /** * Descriptor for a resource name */ export type ResourceDescriptor = { /** * Hard-coded resource names keyed by env types. */ mappedNames?: Partial>; /** * The name of an environment variable that can be used to configure the * resource name */ envVarName?: string; /** * The default resource name if no environment variable is set */ defaultName?: string; /** * Resource name template with optional references to `${envName}`/`${envType}` */ template?: string; }; /** * import from another file as this will grow * maybe something like this * * ```ts * const AWSResourcesDict = yaml.load(readFileSync('./resources-dict.yml', 'utf8')) * as AWSResourcesType; * ``` * Default values for resources */ export const AWSResources: Record = { DynamoDBTable: { template: 'collabland-table-${envName}', mappedNames: { [EnvType.PROD]: 'CollabLand-Prod', [EnvType.STAGING]: 'CollabLand-Staging', [EnvType.QA]: 'CollabLand-QA', }, envVarName: 'COLLABLAND_BOT_TABLE', }, DynamoDBAPITable: { template: 'collabland-api-table-${envName}', mappedNames: { [EnvType.PROD]: 'CollabLandAPI-Prod', [EnvType.STAGING]: 'CollabLandAPI-Staging', [EnvType.QA]: 'CollabLandAPI-QA', }, envVarName: 'COLLABLAND_API_TABLE', }, S3Bucket: { template: 'collabland-s3-bucket-${envName}', mappedNames: { [EnvType.PROD]: 'collabland-api-prod', [EnvType.STAGING]: 'collabland-api-staging', [EnvType.QA]: 'collabland-api-qa', }, envVarName: 'COLLABLAND_S3_BUCKET_NAME', }, SQSJobsQueue: { template: 'collabland-jobs-${envName}', mappedNames: { [EnvType.PROD]: 'collabland-jobs-prod', [EnvType.STAGING]: 'collabland-jobs-staging', [EnvType.QA]: 'collabland-jobs-qa', }, envVarName: 'COLLABLAND_JOBS_QUEUE_NAME', }, SQSJobsJoinQueue: { template: 'collabland-jobs-vip-${envName}', mappedNames: { [EnvType.PROD]: 'collabland-jobs-vip-prod', [EnvType.STAGING]: 'collabland-jobs-vip-staging', [EnvType.QA]: 'collabland-jobs-vip-qa', }, envVarName: 'COLLABLAND_JOBS_JOIN_QUEUE_NAME', }, SQSJobsEventsQueue: { template: 'collabland-jobs-events-${envName}', mappedNames: { [EnvType.PROD]: 'collabland-jobs-events-prod', [EnvType.STAGING]: 'collabland-jobs-events-staging', [EnvType.QA]: 'collabland-jobs-events-qa', }, envVarName: 'COLLABLAND_JOBS_EVENTS_QUEUE_NAME', }, SNSJobsTopic: { template: 'collabland-jobs-${envName}', mappedNames: { [EnvType.PROD]: 'collabland-jobs-prod', [EnvType.STAGING]: 'collabland-jobs-staging', [EnvType.QA]: 'collabland-jobs-qa', }, envVarName: 'COLLABLAND_JOBS_TOPIC_NAME', }, SQSRedPacketClaimsQueue: { template: 'collabland-redpacket-claims-${evnName}', mappedNames: { [EnvType.PROD]: 'collabland-redpacket-claims-prod', [EnvType.STAGING]: 'collabland-redpacket-claims-staging', [EnvType.QA]: 'collabland-redpacket-claims-qa', }, envVarName: 'COLLABLAND_REDPACKET_CLAIMS_QUEUE_NAME', }, SQSTokenClaimsQueue: { template: 'collabland-token-claims-${evnName}', mappedNames: { [EnvType.PROD]: 'collabland-token-claims-prod', [EnvType.STAGING]: 'collabland-token-claims-staging', [EnvType.QA]: 'collabland-token-claims-qa', }, envVarName: 'COLLABLAND_TOKEN_CLAIMS_QUEUE_NAME', }, SQSTokenSimpleClaimsQueue: { template: 'collabland-token-simple-claims-${evnName}', mappedNames: { [EnvType.PROD]: 'collabland-token-simple-claims-prod', [EnvType.STAGING]: 'collabland-token-simple-claims-staging', [EnvType.QA]: 'collabland-token-simple-claims-qa', }, envVarName: 'COLLABLAND_TOKEN_SIMPLE_CLAIMS_QUEUE_NAME', }, KMSKeyAlias: { template: 'alias/collabland-api-key-${envName}', mappedNames: { [EnvType.PROD]: 'alias/collabland-api-key', [EnvType.STAGING]: 'alias/collabland-api-key-staging', [EnvType.QA]: 'alias/collabland-api-key-qa', }, envVarName: 'COLLABLAND_KMS_KEY_ALIAS', }, APIServer: { template: 'CollabLandApiServer-${envName}', mappedNames: { [EnvType.PROD]: 'CollabLandApiServer-Prod', [EnvType.STAGING]: 'CollabLandApiServer-Staging', [EnvType.QA]: 'CollabLandApiServer-QA', }, envVarName: 'COLLABLAND_API_SERVER_NAME', }, SecretName: { template: 'collabland-api/${envName}', mappedNames: { [EnvType.PROD]: 'collabland-api/prod', [EnvType.STAGING]: 'collabland-api/staging', [EnvType.QA]: 'collabland-api/qa', }, envVarName: 'COLLABLAND_SECRET_NAME', }, APIServerURL: { template: 'https://api-${envName}.collab.land', mappedNames: { [EnvType.PROD]: 'https://api.collab.land', [EnvType.STAGING]: 'https://api-staging.collab.land', [EnvType.QA]: 'https://api-qa.collab.land', }, envVarName: 'COLLABLAND_API_SERVER_URL', }, LoginURL: { template: 'https://login-${envName}.collab.land', mappedNames: { [EnvType.PROD]: 'https://login.collab.land', [EnvType.STAGING]: 'https://login-staging.collab.land', // [EnvType.QA]: 'https://login-qa.collab.land', }, envVarName: 'COLLABLAND_LOGIN_URL', }, CommandCenterURL: { template: 'https://cc-${envName}.collab.land', mappedNames: { [EnvType.PROD]: 'https://cc.collab.land', [EnvType.STAGING]: 'https://cc-staging.collab.land', [EnvType.QA]: 'https://cc-qa.collab.land', }, envVarName: 'COLLABLAND_COMMAND_CENTER_URL', }, WalletConnectionURL: { template: 'https://connect-${envName}.collab.land', mappedNames: { [EnvType.PROD]: 'https://connect.collab.land', [EnvType.STAGING]: 'https://staging-connect.collab.land', [EnvType.QA]: 'https://staging-connect.collab.land', }, envVarName: 'COLLABLAND_CONNECT_URL', }, GCPBigqueryDatabaseID: { template: 'collabland_${envName}_analytics', mappedNames: { [EnvType.PROD]: 'collabland_prod_analytics', [EnvType.STAGING]: 'collabland_staging_analytics', [EnvType.QA]: 'collabland_qa_analytics', }, envVarName: 'COLLABLAND_GCP_BIGQUERY_DATABASE_ID', }, GCPBigqueryTableID: { template: 'collabland-events', mappedNames: { [EnvType.PROD]: 'collabland-events', [EnvType.STAGING]: 'collabland-events', [EnvType.QA]: 'collabland-events', }, envVarName: 'COLLABLAND_GCP_BIGQUERY_TABLE_ID', }, GCPBigqueryMixPanelTableID: { template: 'collabland-mixpanel-events', mappedNames: { [EnvType.PROD]: 'collabland-mixpanel-events', [EnvType.STAGING]: 'collabland-mixpanel-events', [EnvType.QA]: 'collabland-mixpanel-events', }, envVarName: 'COLLABLAND_GCP_BIGQUERY_MIXPANEL_TABLE_ID', }, GCPBigqueryTelefrensTableID: { template: 'telefrens-collabland-events', mappedNames: { [EnvType.PROD]: 'telefrens-collabland-events', [EnvType.STAGING]: 'telefrens-collabland-events', [EnvType.QA]: 'telefrens-collabland-events', }, envVarName: 'COLLABLAND_GCP_BIGQUERY_TELEFRENS_TABLE_ID', }, GCPBigqueryTipLitErrorsTableID: { template: 'tip-lit-errors', mappedNames: { [EnvType.PROD]: 'tip-lit-errors', [EnvType.STAGING]: 'tip-lit-errors', [EnvType.QA]: 'tip-lit-errors', }, envVarName: 'COLLABLAND_GCP_BIGQUERY_TIP_LIT_ERRORS_TABLE_ID', }, }; /** * Get the environment full name (team + environment) */ export function getEnvName() { return getEnvVar('COLLABLAND_ENV') ?? getEnvVar('NODE_ENV'); } /** * Get the environment type, default to `EnvType.QA` */ export function getEnv(name?: string): EnvType { const env = name ?? getEnvName(); if (env == null) return EnvType.QA; const parts = env.split('-'); const envType = parts.length === 0 ? parts[0] : parts[parts.length - 1]; switch (envType) { case 'prod': case 'production': return EnvType.PROD; case 'staging': return EnvType.STAGING; case 'qa': return EnvType.QA; case 'test': return EnvType.TEST; case 'dev': case 'development': return EnvType.DEV; default: return EnvType.QA; } } /** * Set the environment name * @param env - Environment name */ export function setEnv(env: EnvType | string) { setEnvVar('COLLABLAND_ENV', env, true); } /** * Check if Collab.Land is running in production mode */ export function isProduction(): boolean { return getEnv() === EnvType.PROD; } /** * Check if Collab.Land running in development mode * @returns */ export function isDevelopment() { return getEnv() === EnvType.DEV; } /** * Get the string value of an environment variable * @param name - Name of the variable * @param defaultValue - Default value * @returns */ export function getEnvVar(name: string, defaultValue: string): string; /** * Get the string value of an environment variable * @param name - Name of the variable * @returns */ export function getEnvVar(name: string): string | undefined; export function getEnvVar( name: string, defaultValue?: string, ): string | undefined { let val = process.env[name]; if (val != null) return val; const secrets = globalThis.collabLandSecrets ?? {}; val = secrets[name]; if (val == null) return defaultValue; if (typeof val === 'string') return val; return JSON.stringify(val); } /** * Get the number value of an environment variable * @param name - Name of the variable * @returns */ export function getEnvVarAsNumber(name: string): number | undefined; /** * Get the number value of an environment variable * @param name - Name of the variable * @param defaultValue - Default value * @returns */ export function getEnvVarAsNumber(name: string, defaultValue: number): number; export function getEnvVarAsNumber( name: string, defaultValue?: number, ): number | undefined { const val = getEnvVar(name); if (val == null) return defaultValue; const num = parseInt(val); if (isNaN(num)) { throw new Error(`The value of "${name}" is not a number: ${val}`); } return num; } /** * Get the environment variable as boolean * @param name - Variable name * @param defaultValue - Default value * @returns false if the lowercase string value is 'false`, '0', 'no', 'n', or '' */ export function getEnvVarAsBoolean( name: string, defaultValue = false, ): boolean { const val = getEnvVar(name); if (val == null) return defaultValue; return !['0', 'false', 'no', 'n', ''].includes(val.toLowerCase()); } /** * Get the object value of an environment variable * @param name - Name of the variable * @returns */ export function getEnvVarAsObject(name: string): T | undefined; /** * Get the object value of an environment variable * @param name - Name of the variable * @param defaultValue - Default value * @returns */ export function getEnvVarAsObject(name: string, defaultValue: T): T; export function getEnvVarAsObject( name: string, defaultValue?: T, ): T | undefined { const val = getEnvVar(name); if (val == null) return defaultValue; try { const obj = JSON.parse(val); return obj; } catch (err) { throw new Error(`The value of "${name}" is not an object: ${val}`); } } /** * Set an environment variable to the given value. It does not override existing * variables. * @param name - Name of the variable * @param value - Value that can be serialized as a string * @param override - Override existing variable * @returns */ export function setEnvVar(name: string, value: unknown, override = false) { if (process.env[name] != null && process.env[name] !== '' && !override) { // Do not override existing environment variable return process.env[name]; } if (value == null) return value; if (typeof value === 'string') { // Set string value process.env[name] = value; return value; } else { // Set number/boolean/array/object values const str = JSON.stringify(value); process.env[name] = str; return str; } } /** * Delete an environment variable * @param name - Name of the variable */ export function unsetEnvVar(name: string) { delete process.env[name]; if (globalThis.collabLandSecrets != null) { delete globalThis.collabLandSecrets[name]; } } /** * Get the resource name * 1. Use a custom resource name configured for the given env * 2. Use the value from an environment variable if it's set * 3. Try to build the name from the template * @param resource - Resource type * @param env * @returns */ export function getResourceName(resource: ResourceType, env?: string): string { if (env == null) { env = getEnvName() ?? EnvType.QA; } const envType = getEnv(env); const descriptor = AWSResources[resource]; if (descriptor == null) { throw new Error(`Resource descriptor ${resource} not found`); } // 1. check if the env has a mapped (hard-coded) resource name let resourceName = descriptor.mappedNames?.[envType]; if (resourceName != null) { return resourceName; } // 2. check if the resource name can be configured via an environment variable if (descriptor.envVarName != null) { resourceName = getEnvVar(descriptor.envVarName); if (resourceName != null) { return resourceName; } // 3. check if there is a default name if the env var is not set if (descriptor.defaultName) { return descriptor.defaultName; } } if (descriptor.template == null) { throw new Error(`Resource ${resource} does not have a template`); } // Build the resource name by convention return template(descriptor.template)({ envName: env, envType, resource, }); } /** * Server types */ export enum ServerType { APIServer = 'APIServer', JobServer = 'JobServer', DiscordServer = 'DiscordServer', CronServer = 'CronServer', } /** * Job server types */ export enum JobServerType { /** * The job server that handles `join` flow */ join = 'join', /** * The job server that runs background checks */ background = 'background', /** * The job server that runs event-driven checks */ events = 'events', } /** * Get the job server type from the env var * @returns */ export function getJobServerType(): JobServerType { const jobServerType = getEnvVar( 'COLLABLAND_JOB_SERVER_TYPE', JobServerType.join, ); switch (jobServerType.toLowerCase()) { case JobServerType.join: return JobServerType.join; case JobServerType.background: return JobServerType.background; case JobServerType.events: return JobServerType.events; default: return JobServerType.join; } } /** * API server types */ export enum APIServerType { main = 'main', claim = 'claim', } /** * Get the api server type from the env var * @returns */ export function getAPIServerType() { const apiServerType = getEnvVar( 'COLLABLAND_API_SERVER_TYPE', APIServerType.main, ); switch (apiServerType.toLowerCase()) { case APIServerType.main: case APIServerType.claim: return apiServerType; default: return APIServerType.main; } } export function getCommandCenterURL() { return getResourceName(ResourceType.CommandCenterURL); } /** * Get the base url of CollabLand API server * @returns Base url of CollabLand API server */ export function getApiServerURL() { return getResourceName(ResourceType.APIServerURL); } export function getLoginURL() { return getResourceName(ResourceType.LoginURL); } export function getWalletConnectionURL() { return getResourceName(ResourceType.WalletConnectionURL); } export function getWalletConnectionDomain() { const url = getWalletConnectionURL(); return new URL(url).hostname; } /** * Strip the extension from a filename if it has one. * @param name - A filename. * @return The filename without a path. */ export function stripExt(name: string) { const extension = path.extname(name); if (!extension) { return name; } return name.slice(0, -extension.length); } /** * Check if the given module is the main entry * @param module - `import.meta.url` for ESM or `module` for CommonJS * @returns */ export function isMain(module: string | Module) { if (typeof module !== 'string') { return require.main === module; } else { const require = createRequire(module); const scriptPath = require.resolve(process.argv[1]); const modulePath = fileURLToPath(module); const extension = path.extname(scriptPath); if (extension) { return modulePath === scriptPath; } return stripExt(modulePath) === scriptPath; } }