import * as process from "process"; import * as convict from "convict"; import * as Metamigo from "@digiresilience/metamigo-common"; import { defState } from "@digiresilience/montar"; export const configSchema = { db: { connection: { doc: "The postgres connection url.", format: "uri", default: "postgresql://metamigo:metamigo@127.0.0.1:5433/metamigo_dev", env: "METAMIGO_DATABASE_URL", sensitive: true, }, name: { doc: "The name of the postgres database", format: String, default: "metamigo_dev", env: "METAMIGO_DATABASE_NAME", }, owner: { doc: "The username of the postgres database owner", format: String, default: "metamigo", env: "METAMIGO_DATABASE_OWNER", }, }, worker: { connection: { doc: "The postgres connection url for the worker database.", format: "uri", default: "postgresql://metamigo:metamigo@127.0.0.1:5433/metamigo_dev", env: "METAMIGO_WORKER_DATABASE_URL", }, concurrency: { doc: "The number of jobs to run concurrently", default: 1, format: "positiveInt", env: "METAMIGO_WORKER_CONCURRENT_JOBS", }, pollInterval: { doc: "How long to wait between polling for jobs in milliseconds (for jobs scheduled in the future/retries)", default: 2000, format: "positiveInt", env: "METAMIGO_WORKER_POLL_INTERVAL_MS", }, }, postgraphile: { auth: { doc: "The postgres role that postgraphile logs in with", format: String, default: "metamigo_graphile_auth", env: "METAMIGO_DATABASE_AUTHENTICATOR", }, appRootConnection: { doc: "The postgres root/superuser connection url for development mode so PG can watch the schema changes, this is strangely named in the postgraphile API 'ownerConnectionString'", format: String, default: "postgresql://postgres:metamigo@127.0.0.1:5433/metamigo_dev", env: "METAMIGO_APP_ROOT_DATABASE_URL", }, authConnection: { doc: "The postgres connection URL for postgraphile, must not be superuser and must have limited privs.", format: String, default: "postgresql://metamigo_graphile_auth:metamigo@127.0.0.1:5433/metamigo_dev", env: "METAMIGO_DATABASE_AUTH_URL", }, visitor: { doc: "The postgres role that postgraphile switches to", format: String, default: "app_postgraphile", env: "METAMIGO_DATABASE_VISITOR", }, schema: { doc: "The schema postgraphile should expose with graphql", format: String, default: "app_public", }, enableGraphiql: { doc: "Whether to enable the graphiql web interface or not", format: "Boolean", default: false, env: "METAMIGO_ENABLE_GRAPHIQL", }, }, dev: { shadowConnection: { doc: "The shadow databse connection url used by postgraphile-migrate. Not needed in production.", format: "uri", default: "postgresql://metamigo:metamigo@127.0.0.1:5433/metamigo_shadow", env: "METAMIGO_SHADOW_DATABASE_URL", sensitive: true, }, rootConnection: { doc: "The postgres root/superuser connection url for testing only, database must NOT be the app database. Not needed in production.", format: "uri", default: "postgresql://postgres:metamigo@127.0.0.1:5433/template1", env: "METAMIGO_ROOT_DATABASE_URL", sensitive: true, }, }, frontend: { url: { doc: "The url the frontend can be accessed at", format: "url", default: "http://localhost:3000", env: "METAMIGO_FRONTEND_URL", }, apiUrl: { doc: "The url the api backend can be accessed at from the frontend server", format: "url", default: "http://localhost:3001", env: "METAMIGO_API_URL", }, }, nextAuth: { secret: { doc: "A random string used to hash tokens, sign cookies and generate crytographic keys. Shared with the api backend.", format: String, default: undefined, env: "NEXTAUTH_SECRET", sensitive: true, }, audience: { doc: "We will add this string as the `aud` claim to our JWT token, if empty or not present defaults to `frontend.url`", format: String, default: "", env: "NEXTAUTH_AUDIENCE", }, signingKeyB64: { doc: "A base64 encoded JWK.Key used for JWT signing", format: String, default: undefined, env: "NEXTAUTH_SIGNING_KEY_B64", sensitive: true, }, encryptionKeyB64: { doc: "A base64 encoded JWK.Key used for JWT encryption", format: String, default: undefined, env: "NEXTAUTH_ENCRYPTION_KEY_B64", sensitive: true, }, signingKey: { doc: "", format: String, default: undefined, sensitive: true, skipGenerate: true, }, encryptionKey: { doc: "", format: String, default: undefined, sensitive: true, skipGenerate: true, }, google: { id: { doc: "reference https://next-auth.js.org/providers/google", format: String, default: undefined, env: "GOOGLE_ID", sensitive: true, }, secret: { doc: "reference https://next-auth.js.org/providers/google", format: String, default: undefined, env: "GOOGLE_SECRET", sensitive: true, }, }, github: { id: { doc: "reference https://next-auth.js.org/providers/github", format: String, default: undefined, env: "GITHUB_ID", sensitive: true, }, secret: { doc: "reference https://next-auth.js.org/providers/github", format: String, default: undefined, env: "GITHUB_SECRET", sensitive: true, }, }, gitlab: { id: { doc: "reference https://next-auth.js.org/providers/gitlab", format: String, default: undefined, env: "GITLAB_ID", sensitive: true, }, secret: { doc: "reference https://next-auth.js.org/providers/gitlab", format: String, default: undefined, env: "GITLAB_SECRET", sensitive: true, }, }, cognito: { id: { doc: "reference https://next-auth.js.org/providers/cognito", format: String, default: undefined, env: "COGNITO_ID", sensitive: true, }, secret: { doc: "reference https://next-auth.js.org/providers/cognito", format: String, default: undefined, env: "COGNITO_SECRET", sensitive: true, }, domain: { doc: "reference https://next-auth.js.org/providers/cognito", format: String, default: undefined, env: "COGNITO_DOMAIN", sensitive: true, }, }, }, cfaccess: { audience: { doc: "the cloudflare access audience id", format: String, default: undefined, env: "CFACCESS_AUDIENCE", }, domain: { doc: "the cloudflare access domain, something like `YOURAPP.cloudflareaccess.com`", format: String, default: undefined, env: "CFACCESS_DOMAIN", }, }, signald: { enabled: { doc: "Whether to enable the signald signal backend", format: "Boolean", default: false, env: "SIGNALD_ENABLED", }, socket: { doc: "the unix domain socket signald is listening on", format: String, default: `${process.cwd()}/signald/signald.sock`, env: "SIGNALD_SOCKET", }, }, leafcutter: { enabled: { doc: "Whether to enable leafcutter functionality", format: "Boolean", default: false, env: "LEAFCUTTER_ENABLED", }, zammadApiUrl: { doc: "The full base zammad api url", format: String, default: undefined, env: "ZAMMAD_API_URL", }, zammadApiKey: { doc: "The zammad api key", format: String, default: undefined, sensitive: true, env: "ZAMMAD_API_KEY", }, labelStudioApiUrl: { doc: "The full base label studio api url", format: String, default: undefined, env: "LABEL_STUDIO_API_URL", }, labelStudioApiKey: { doc: "The label studio api key", format: String, default: undefined, sensitive: true, env: "LABEL_STUDIO_API_KEY", }, contributorId: { doc: "The leafcutter contributor id", format: String, default: undefined, env: "LEAFCUTTER_CONTRIBUTOR_ID", }, contributorName: { doc: "The leafcutter contributor name", format: String, default: undefined, env: "LEAFCUTTER_CONTRIBUTOR_NAME", }, opensearchApiUrl: { doc: "The opensearch api url", format: String, default: undefined, env: "OPENSEARCH_API_URL", }, opensearchUsername: { doc: "The opensearch username", format: String, default: undefined, env: "OPENSEARCH_USERNAME", }, opensearchPassword: { doc: "The opensearch password", format: String, default: undefined, sensative: true, env: "OPENSEARCH_PASSWORD", }, }, }; // define the interfaces for the concrete config objects export interface IDBConfig { connection: string; name: string; owner: string; } export interface IWorkerConfig { connection: string; concurrency: number; pollInterval: number; } export interface IPostgraphileConfig { auth: string; visitor: string; appRootConnection: string; authConnection: string; schema: string; enableGraphiql: boolean; } export interface IDevConfig { shadowConnection: string; rootConnection: string; } export interface IFrontendConfig { url: string; apiUrl: string; } export interface INextAuthConfig { secret: string; audience: string; signingKey: string; encryptionKey: string; signingKeyB64: string; encryptionKeyB64: string; google?: { id: string; secret: string; }; github?: { id: string; secret: string; }; gitlab?: { id: string; secret: string; }; cognito?: { id: string; secret: string; domain: string; }; } export interface ICFAccessConfig { audience: string; domain: string; } export interface ISignaldConifg { enabled: boolean; socket: string; } export interface ILeafcutterConfig { enabled: boolean; zammadApiUrl: string; zammadApiKey: string; labelStudioApiUrl: string; labelStudioApiKey: string; contributorId: string; contributorName: string; opensearchApiUrl: string; opensearchUsername: string; opensearchPassword: string; } // Extend the metamigo base type to add your app's custom config along side the out // of the box Metamigo config export interface IAppConfig extends Metamigo.IMetamigoConfig { db: IDBConfig; worker: IWorkerConfig; postgraphile: IPostgraphileConfig; dev: IDevConfig; frontend: IFrontendConfig; nextAuth: INextAuthConfig; cfaccess: ICFAccessConfig; signald: ISignaldConifg; leafcutter: ILeafcutterConfig; } export type IAppConvict = Metamigo.ExtendedConvict; // Merge the Metamigo base schema with your app's schmea export const schema: convict.Schema = { ...Metamigo.configBaseSchema, ...configSchema, }; export const loadConfig = async (): Promise => { const config = await Metamigo.loadConfiguration(schema); if (!config.frontend.url || config.frontend.url === "") throw new Error( "configuration value frontend.url is missing. Add to config or set NEXTAUTH_URL env var" ); // nextauth expects the url to be provided with this environment variable, so we will munge it in place here process.env.NEXTAUTH_URL = config.frontend.url; if (config.nextAuth.signingKeyB64) config.nextAuth.signingKey = Buffer.from( config.nextAuth.signingKeyB64, "base64" ).toString("utf-8"); if (config.nextAuth.encryptionKeyB64) config.nextAuth.encryptionKey = Buffer.from( config.nextAuth.encryptionKeyB64, "base64" ).toString("utf-8"); if (!config.nextAuth.audience || config.nextAuth.audience === "") config.nextAuth.audience = config.frontend.url; return config as IAppConfig; }; export const loadConfigRaw = async (): Promise => Metamigo.loadConfigurationRaw(schema); const config = defState("config", { start: loadConfig, }); export default config;