diff --git a/.env.example b/.env.example deleted file mode 100644 index c5daeb201..000000000 --- a/.env.example +++ /dev/null @@ -1,29 +0,0 @@ -LITEFS_DIR="/litefs/data" -DATABASE_PATH="./prisma/data.db" -DATABASE_URL="file:./data.db?connection_limit=1" -CACHE_DATABASE_PATH="./other/cache.db" -SESSION_SECRET="super-duper-s3cret" -HONEYPOT_SECRET="super-duper-s3cret" -RESEND_API_KEY="re_blAh_blaHBlaHblahBLAhBlAh" -SENTRY_DSN="your-dsn" - -# this is set to a random value in the Dockerfile -INTERNAL_COMMAND_TOKEN="some-made-up-token" - -# the mocks and some code rely on these two being prefixed with "MOCK_" -# if they aren't then the real github api will be attempted -GITHUB_CLIENT_ID="MOCK_GITHUB_CLIENT_ID" -GITHUB_CLIENT_SECRET="MOCK_GITHUB_CLIENT_SECRET" -GITHUB_TOKEN="MOCK_GITHUB_TOKEN" -GITHUB_REDIRECT_URI="https://example.com/auth/github/callback" - -# set this to false to prevent search engines from indexing the website -# default to allow indexing for seo safety -ALLOW_INDEXING="true" - -# Tigris Object Storage (S3-compatible) Configuration -AWS_ACCESS_KEY_ID="mock-access-key" -AWS_SECRET_ACCESS_KEY="mock-secret-key" -AWS_REGION="auto" -AWS_ENDPOINT_URL_S3="https://fly.storage.tigris.dev" -BUCKET_NAME="mock-bucket" diff --git a/.env.schema b/.env.schema new file mode 100644 index 000000000..5f242dc3e --- /dev/null +++ b/.env.schema @@ -0,0 +1,102 @@ +# This env file uses @env-spec - see https://varlock.dev/env-spec for more info +# +# @defaultRequired=true @defaultSensitive=false +# @currentEnv=$NODE_ENV +# @generateTypes(lang=ts, path=types/env-vars.d.ts) +# ---------- + +# @type=enum(development, production, test) +NODE_ENV=development +# @type=enum(development, production, test) +MODE=$NODE_ENV + +# @type=port +PORT=3000 + +LITEFS_DIR="/litefs/data" +DATABASE_PATH="./prisma/data.db" +DATABASE_URL="file:./data.db?connection_limit=1" +CACHE_DATABASE_PATH="./other/cache.db" + +# used to secure sessions +# @sensitive +# @docs(https://stack-staging.epicweb.dev/topic/deployment) +SESSION_SECRET="super-duper-s3cret" + +# encryption seed for honeypot server +# @sensitive +# @docs(https://stack-staging.epicweb.dev/topic/deployment) +HONEYPOT_SECRET="super-duper-s3cret" + +# this is set to a random value in the Dockerfile +# @sensitive +INTERNAL_COMMAND_TOKEN="some-made-up-token" + +# set to false to prevent search engines from indexing the website (defaults to allow) +ALLOW_INDEXING=true + +# enables mocks for external services +MOCKS=forEnv(development, test) + +# will be set to curent commit sha in deployments +# @optional +COMMIT_SHA= + +# API key for Resend (email service) +# @type=string(startsWith=re_) +# @sensitive +# @optional # remove this if using resend +# @docs(https://resend.com/docs/dashboard/api-keys/introduction#what-is-an-api-key) +RESEND_API_KEY= + +# will be set to true when running in CI +CI=false + +# Sentry settings (error tracking) +# note that SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT are optional +# but enable @sentry/react-router integration and release tagging +# --- +# @type=url +# @optional # remove this if using sentry +# @example=https://examplePublicKey@o0.ingest.sentry.io/0 +# @docs(https://docs.sentry.io/concepts/key-terms/dsn-explainer/) +SENTRY_DSN= +# @optional @sensitive +SENTRY_AUTH_TOKEN= +# @required=if($SENTRY_AUTH_TOKEN) +SENTRY_ORG= +# @required=if($SENTRY_AUTH_TOKEN) +SENTRY_PROJECT= + +# GitHub settings +# +# the mocks and some code rely on these being prefixed with "MOCK_" +# if they aren't then the real github api will be attempted +# --- +GITHUB_CLIENT_ID="MOCK_GITHUB_CLIENT_ID" +# @sensitive +GITHUB_CLIENT_SECRET="MOCK_GITHUB_CLIENT_SECRET" +# @sensitive +GITHUB_TOKEN="MOCK_GITHUB_TOKEN" +# @type=url +GITHUB_REDIRECT_URI="https://example.com/auth/github/callback" + + +# Tigris Object Storage (S3-compatible) Configuration +# --- +AWS_ACCESS_KEY_ID="mock-access-key" +# @sensitive +AWS_SECRET_ACCESS_KEY="mock-secret-key" +AWS_REGION="auto" +# @type=url +AWS_ENDPOINT_URL_S3="https://fly.storage.tigris.dev" +BUCKET_NAME="mock-bucket" + +# Populated by fly.io +# --- +# current fly.io region +# @optional +FLY_REGION= +# app name as set in fly.io +# @optional +FLY_APP_NAME= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2345034e4..ef74da54f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ node_modules /build /server-build .env +.env.local +.env.*.local .cache /prisma/data.db @@ -26,3 +28,4 @@ node_modules # generated files /app/components/ui/icons .react-router/ +/types/env-vars.d.ts diff --git a/.prettierignore b/.prettierignore index f022d0280..81afeda3b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,6 +4,7 @@ node_modules /public/build /server-build .env +.env.* /test-results/ /playwright-report/ @@ -11,5 +12,6 @@ node_modules /tests/fixtures/email/*.json /coverage /prisma/migrations +/types/env-vars.d.ts package-lock.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 3c0a690df..7eb65d6b6 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -6,6 +6,7 @@ "prisma.prisma", "qwtel.sqlite-viewer", "yoavbls.pretty-ts-errors", - "github.vscode-github-actions" + "github.vscode-github-actions", + "varlock.env-spec-language" ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b39faf8c..9c1c0071c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,10 +18,9 @@ instructions: 1. Fork repo 2. clone the repo -3. Copy `.env.example` into `.env` -4. Run `npm install && npm run setup -s` to install dependencies and run +3. Run `npm install && npm run setup -s` to install dependencies and run validation -5. Create a branch for your PR with `git checkout -b pr/your-branch-name` +4. Create a branch for your PR with `git checkout -b pr/your-branch-name` > Tip: Keep your `main` branch pointing at the original repository and make pull > requests from branches on your fork. To do this, run: @@ -44,10 +43,10 @@ If the setup script doesn't work, you can try to run the commands manually: git clone cd ./epic-stack -# copy the .env.example to .env +# create a file for gitignored .env overrides # everything's mocked out during development so you shouldn't need to -# change any of these values unless you want to hit real environments. -cp .env.example .env +# set anything unless you want to hit real environments. +touch .env.local # Install deps npm install diff --git a/app/entry.client.tsx b/app/entry.client.tsx index 9b7749f3a..69984510f 100644 --- a/app/entry.client.tsx +++ b/app/entry.client.tsx @@ -1,6 +1,7 @@ import { startTransition } from 'react' import { hydrateRoot } from 'react-dom/client' import { HydratedRouter } from 'react-router/dom' +import { ENV } from 'varlock/env' if (ENV.MODE === 'production' && ENV.SENTRY_DSN) { void import('./utils/monitoring.client.tsx').then(({ init }) => init()) diff --git a/app/entry.server.tsx b/app/entry.server.tsx index 99fdd4b88..f1c7cbda9 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -12,17 +12,13 @@ import { type ActionFunctionArgs, type HandleDocumentRequestFunction, } from 'react-router' -import { getEnv, init } from './utils/env.server.ts' +import { ENV } from 'varlock/env' import { getInstanceInfo } from './utils/litefs.server.ts' import { NonceProvider } from './utils/nonce-provider.ts' import { makeTimings } from './utils/timing.server.ts' export const streamTimeout = 5000 -init() -global.ENV = getEnv() - -const MODE = process.env.NODE_ENV ?? 'development' type DocRequestArgs = Parameters @@ -30,12 +26,12 @@ export default async function handleRequest(...args: DocRequestArgs) { const [request, responseStatusCode, responseHeaders, reactRouterContext] = args const { currentInstance, primaryInstance } = await getInstanceInfo() - responseHeaders.set('fly-region', process.env.FLY_REGION ?? 'unknown') - responseHeaders.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown') + responseHeaders.set('fly-region', ENV.FLY_REGION ?? 'unknown') + responseHeaders.set('fly-app', ENV.FLY_APP_NAME ?? 'unknown') responseHeaders.set('fly-primary-instance', primaryInstance) responseHeaders.set('fly-instance', currentInstance) - if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) { + if (ENV.NODE_ENV === 'production' && ENV.SENTRY_DSN) { responseHeaders.append('Document-Policy', 'js-profiling') } @@ -72,8 +68,8 @@ export default async function handleRequest(...args: DocRequestArgs) { directives: { fetch: { 'connect-src': [ - MODE === 'development' ? 'ws:' : undefined, - process.env.SENTRY_DSN ? '*.sentry.io' : undefined, + ENV.MODE === 'development' ? 'ws:' : undefined, + ENV.SENTRY_DSN ? '*.sentry.io' : undefined, "'self'", ], 'font-src': ["'self'"], @@ -114,8 +110,8 @@ export default async function handleRequest(...args: DocRequestArgs) { export async function handleDataRequest(response: Response) { const { currentInstance, primaryInstance } = await getInstanceInfo() - response.headers.set('fly-region', process.env.FLY_REGION ?? 'unknown') - response.headers.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown') + response.headers.set('fly-region', ENV.FLY_REGION ?? 'unknown') + response.headers.set('fly-app', ENV.FLY_APP_NAME ?? 'unknown') response.headers.set('fly-primary-instance', primaryInstance) response.headers.set('fly-instance', currentInstance) diff --git a/app/root.tsx b/app/root.tsx index 3b19435c1..93038c125 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -11,6 +11,7 @@ import { useMatches, } from 'react-router' import { HoneypotProvider } from 'remix-utils/honeypot/react' +import { ENV } from 'varlock/env' import { type Route } from './+types/root.ts' import appleTouchIconAssetUrl from './assets/favicons/apple-touch-icon.png' import faviconAssetUrl from './assets/favicons/favicon.svg' @@ -31,7 +32,6 @@ import tailwindStyleSheetUrl from './styles/tailwind.css?url' import { getUserId, logout } from './utils/auth.server.ts' import { ClientHintCheck, getHints } from './utils/client-hints.tsx' import { prisma } from './utils/db.server.ts' -import { getEnv } from './utils/env.server.ts' import { pipeHeaders } from './utils/headers.server.ts' import { honeypot } from './utils/honeypot.server.ts' import { combineHeaders, getDomainUrl, getImgSrc } from './utils/misc.tsx' @@ -119,7 +119,6 @@ export async function loader({ request }: Route.LoaderArgs) { theme: getTheme(request), }, }, - ENV: getEnv(), toast, honeyProps, }, @@ -138,14 +137,11 @@ function Document({ children, nonce, theme = 'light', - env = {}, }: { children: React.ReactNode nonce: string theme?: Theme - env?: Record }) { - const allowIndexing = ENV.ALLOW_INDEXING !== 'false' return ( @@ -153,19 +149,13 @@ function Document({ - {allowIndexing ? null : ( + {ENV.ALLOW_INDEXING ? null : ( )} {children} -