Switch to using t3-env for env-var management (#230)

This commit is contained in:
Brendan Kellam 2025-03-17 21:22:05 -07:00 committed by GitHub
parent e8acfcca70
commit 483217bf56
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 404 additions and 484 deletions

View file

@ -1,11 +1,13 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
!.next/static
!.next/standalone
.git
.sourcebot
.env.local
packages/web/.next
!packages/web/.next/static
!packages/web/.next/standalone
**/node_modules
**/.env.local
**/.sentryclirc
**/.env.sentry-build-plugin

View file

@ -1,5 +1,18 @@
# ------ Global scope variables ------
# Set of global build arguments.
# @see: https://docs.docker.com/build/building/variables/#scoping
ARG SOURCEBOT_VERSION
# PAPIK = Project API Key
# Note that this key does not need to be kept secret, so it's not
# necessary to use Docker build secrets here.
# @see: https://posthog.com/tutorials/api-capture-events#authenticating-with-the-project-api-key
ARG POSTHOG_PAPIK
ARG SENTRY_ENVIRONMENT
FROM node:20-alpine3.19 AS node-alpine
FROM golang:1.23.4-alpine3.19 AS go-alpine
# ----------------------------------
# ------ Build Zoekt ------
FROM go-alpine AS zoekt-builder
@ -9,6 +22,7 @@ COPY vendor/zoekt/go.mod vendor/zoekt/go.sum ./
RUN go mod download
COPY vendor/zoekt ./
RUN CGO_ENABLED=0 GOOS=linux go build -o /cmd/ ./cmd/...
# -------------------------
# ------ Build shared libraries ------
FROM node-alpine AS shared-libs-builder
@ -23,9 +37,24 @@ RUN yarn workspace @sourcebot/db install --frozen-lockfile
RUN yarn workspace @sourcebot/schemas install --frozen-lockfile
RUN yarn workspace @sourcebot/crypto install --frozen-lockfile
RUN yarn workspace @sourcebot/error install --frozen-lockfile
# ------------------------------------
# ------ Build Web ------
FROM node-alpine AS web-builder
ENV DOCKER_BUILD=1
# -----------
# Global args
ARG SOURCEBOT_VERSION
ENV NEXT_PUBLIC_SOURCEBOT_VERSION=$SOURCEBOT_VERSION
ARG POSTHOG_PAPIK
ENV NEXT_PUBLIC_POSTHOG_PAPIK=$POSTHOG_PAPIK
ARG SENTRY_ENVIRONMENT
ENV NEXT_PUBLIC_SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT
# Local args
ARG SENTRY_WEBAPP_DSN
ENV NEXT_PUBLIC_SENTRY_WEBAPP_DSN=$SENTRY_WEBAPP_DSN
# -----------
RUN apk add --no-cache libc6-compat
WORKDIR /app
@ -43,26 +72,13 @@ RUN yarn config set network-timeout 1200000
RUN yarn workspace @sourcebot/web install --frozen-lockfile
ENV NEXT_TELEMETRY_DISABLED=1
# @see: https://phase.dev/blog/nextjs-public-runtime-variables/
ARG NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED=BAKED_NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED
ARG NEXT_PUBLIC_SOURCEBOT_VERSION=BAKED_NEXT_PUBLIC_SOURCEBOT_VERSION
ENV NEXT_PUBLIC_PUBLIC_SEARCH_DEMO=BAKED_NEXT_PUBLIC_PUBLIC_SEARCH_DEMO
ENV NEXT_PUBLIC_POSTHOG_PAPIK=BAKED_NEXT_PUBLIC_POSTHOG_PAPIK
ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=BAKED_NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
ENV NEXT_PUBLIC_SENTRY_ENVIRONMENT=BAKED_NEXT_PUBLIC_SENTRY_ENVIRONMENT
ENV NEXT_PUBLIC_SENTRY_WEBAPP_DSN=BAKED_NEXT_PUBLIC_SENTRY_WEBAPP_DSN
# @nocheckin: This was interfering with the the `matcher` regex in middleware.ts,
# causing regular expressions parsing errors when making a request. It's unclear
# why exactly this was happening, but it's likely due to a bad replacement happening
# in the `sed` command.
# @note: leading "/" is required for the basePath property. @see: https://nextjs.org/docs/app/api-reference/next-config-js/basePath
# ARG NEXT_PUBLIC_DOMAIN_SUB_PATH=/BAKED_NEXT_PUBLIC_DOMAIN_SUB_PATH
RUN yarn workspace @sourcebot/web build
ENV DOCKER_BUILD=0
# ------------------------------
# ------ Build Backend ------
FROM node-alpine AS backend-builder
ENV DOCKER_BUILD=1
WORKDIR /app
COPY package.json yarn.lock* ./
@ -75,10 +91,22 @@ COPY --from=shared-libs-builder /app/packages/crypto ./packages/crypto
COPY --from=shared-libs-builder /app/packages/error ./packages/error
RUN yarn workspace @sourcebot/backend install --frozen-lockfile
RUN yarn workspace @sourcebot/backend build
ENV DOCKER_BUILD=0
# ------------------------------
# ------ Runner ------
FROM node-alpine AS runner
# -----------
# Global args
ARG SOURCEBOT_VERSION
ENV SOURCEBOT_VERSION=$SOURCEBOT_VERSION
ARG POSTHOG_PAPIK
ENV POSTHOG_PAPIK=$POSTHOG_PAPIK
# Local args
# -----------
RUN echo "Sourcebot Version: $SOURCEBOT_VERSION"
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
@ -90,14 +118,6 @@ ENV DATABASE_URL="postgresql://postgres@localhost:5432/sourcebot"
ENV REDIS_URL="redis://localhost:6379"
ENV SRC_TENANT_ENFORCEMENT_MODE=strict
ARG SOURCEBOT_VERSION=unknown
ENV SOURCEBOT_VERSION=$SOURCEBOT_VERSION
RUN echo "Sourcebot Version: $SOURCEBOT_VERSION"
ARG PUBLIC_SEARCH_DEMO=false
ENV PUBLIC_SEARCH_DEMO=$PUBLIC_SEARCH_DEMO
RUN echo "Public Search Demo: $PUBLIC_SEARCH_DEMO"
# Valid values are: debug, info, warn, error
ENV SOURCEBOT_LOG_LEVEL=info
@ -106,18 +126,9 @@ ENV SOURCEBOT_LOG_LEVEL=info
# will serve from http(s)://example.com/sb
ENV DOMAIN_SUB_PATH=/
# PAPIK = Project API Key
# Note that this key does not need to be kept secret, so it's not
# necessary to use Docker build secrets here.
# @see: https://posthog.com/tutorials/api-capture-events#authenticating-with-the-project-api-key
ARG POSTHOG_PAPIK=
ENV POSTHOG_PAPIK=$POSTHOG_PAPIK
# Sourcebot collects anonymous usage data using [PostHog](https://posthog.com/). Uncomment this line to disable.
# ENV SOURCEBOT_TELEMETRY_DISABLED=1
ENV STRIPE_PUBLISHABLE_KEY=""
# Configure zoekt
COPY vendor/zoekt/install-ctags-alpine.sh .
RUN ./install-ctags-alpine.sh && rm install-ctags-alpine.sh
@ -178,4 +189,5 @@ COPY default-config.json .
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
ENTRYPOINT ["/sbin/tini", "--", "./entrypoint.sh"]
ENTRYPOINT ["/sbin/tini", "--", "./entrypoint.sh"]
# ------------------------------

View file

@ -108,102 +108,6 @@ fi
echo "{\"version\": \"$SOURCEBOT_VERSION\", \"install_id\": \"$SOURCEBOT_INSTALL_ID\"}" > "$FIRST_RUN_FILE"
# Update NextJs public env variables w/o requiring a rebuild.
# @see: https://phase.dev/blog/nextjs-public-runtime-variables/
{
# Infer NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED if it is not set
if [ -z "$NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED" ] && [ ! -z "$SOURCEBOT_TELEMETRY_DISABLED" ]; then
export NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED="$SOURCEBOT_TELEMETRY_DISABLED"
fi
# Infer NEXT_PUBLIC_SOURCEBOT_VERSION if it is not set
if [ -z "$NEXT_PUBLIC_SOURCEBOT_VERSION" ] && [ ! -z "$SOURCEBOT_VERSION" ]; then
export NEXT_PUBLIC_SOURCEBOT_VERSION="$SOURCEBOT_VERSION"
fi
# Infer NEXT_PUBLIC_PUBLIC_SEARCH_DEMO if it is not set
if [ -z "$NEXT_PUBLIC_PUBLIC_SEARCH_DEMO" ] && [ ! -z "$PUBLIC_SEARCH_DEMO" ]; then
export NEXT_PUBLIC_PUBLIC_SEARCH_DEMO="$PUBLIC_SEARCH_DEMO"
fi
# Always infer NEXT_PUBLIC_POSTHOG_PAPIK
export NEXT_PUBLIC_POSTHOG_PAPIK="$POSTHOG_PAPIK"
# Always infer NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
export NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="$STRIPE_PUBLISHABLE_KEY"
# Always infer NEXT_PUBLIC_SENTRY_ENVIRONMENT
export NEXT_PUBLIC_SENTRY_ENVIRONMENT="$SENTRY_ENVIRONMENT"
# Always infer NEXT_PUBLIC_SENTRY_WEBAPP_DSN
export NEXT_PUBLIC_SENTRY_WEBAPP_DSN="$SENTRY_WEBAPP_DSN"
# Iterate over all .js files in .next & public, making substitutions for the `BAKED_` sentinal values
# with their actual desired runtime value.
find /app/packages/web/public /app/packages/web/.next -type f -name "*.js" |
while read file; do
sed -i "s|BAKED_NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED|${NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED}|g" "$file"
sed -i "s|BAKED_NEXT_PUBLIC_SOURCEBOT_VERSION|${NEXT_PUBLIC_SOURCEBOT_VERSION}|g" "$file"
sed -i "s|BAKED_NEXT_PUBLIC_POSTHOG_PAPIK|${NEXT_PUBLIC_POSTHOG_PAPIK}|g" "$file"
sed -i "s|BAKED_NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY|${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY}|g" "$file"
sed -i "s|BAKED_NEXT_PUBLIC_SENTRY_ENVIRONMENT|${NEXT_PUBLIC_SENTRY_ENVIRONMENT}|g" "$file"
sed -i "s|BAKED_NEXT_PUBLIC_SENTRY_WEBAPP_DSN|${NEXT_PUBLIC_SENTRY_WEBAPP_DSN}|g" "$file"
sed -i "s|BAKED_NEXT_PUBLIC_PUBLIC_SEARCH_DEMO|${NEXT_PUBLIC_PUBLIC_SEARCH_DEMO}|g" "$file"
done
}
# @nocheckin: This was interfering with the the `matcher` regex in middleware.ts,
# causing regular expressions parsing errors when making a request. It's unclear
# why exactly this was happening, but it's likely due to a bad replacement happening
# in the `sed` command.
#
# # Update specifically NEXT_PUBLIC_DOMAIN_SUB_PATH w/o requiring a rebuild.
# # Ultimately, the DOMAIN_SUB_PATH sets the `basePath` param in the next.config.mjs.
# # Similar to above, we pass in a `BAKED_` sentinal value into next.config.mjs at build
# # time. Unlike above, the `basePath` configuration is set in files other than just javascript
# # code (e.g., manifest files, css files, etc.), so this section has subtle differences.
# #
# # @see: https://nextjs.org/docs/app/api-reference/next-config-js/basePath
# # @see: https://phase.dev/blog/nextjs-public-runtime-variables/
# {
# if [ ! -z "$DOMAIN_SUB_PATH" ]; then
# # If the sub-path is "/", this creates problems with certain replacements. For example:
# # /BAKED_NEXT_PUBLIC_DOMAIN_SUB_PATH/_next/image -> //_next/image (notice the double slash...)
# # To get around this, we default to an empty sub-path, which is the default when no sub-path is defined.
# if [ "$DOMAIN_SUB_PATH" = "/" ]; then
# DOMAIN_SUB_PATH=""
# # Otherwise, we need to ensure that the sub-path starts with a slash, since this is a requirement
# # for the basePath property. For example, assume DOMAIN_SUB_PATH=/bot, then:
# # /BAKED_NEXT_PUBLIC_DOMAIN_SUB_PATH/_next/image -> /bot/_next/image
# elif [[ ! "$DOMAIN_SUB_PATH" =~ ^/ ]]; then
# DOMAIN_SUB_PATH="/$DOMAIN_SUB_PATH"
# fi
# fi
# if [ ! -z "$DOMAIN_SUB_PATH" ]; then
# echo -e "\e[34m[Info] DOMAIN_SUB_PATH was set to "$DOMAIN_SUB_PATH". Overriding default path.\e[0m"
# fi
# # Always set NEXT_PUBLIC_DOMAIN_SUB_PATH to DOMAIN_SUB_PATH (even if it is empty!!)
# export NEXT_PUBLIC_DOMAIN_SUB_PATH="$DOMAIN_SUB_PATH"
# # Iterate over _all_ files in the web directory, making substitutions for the `BAKED_` sentinal values
# # with their actual desired runtime value.
# find /app/packages/web -type f |
# while read file; do
# # @note: the leading "/" is required here as it is included at build time. See Dockerfile.
# sed -i "s|/BAKED_NEXT_PUBLIC_DOMAIN_SUB_PATH|${NEXT_PUBLIC_DOMAIN_SUB_PATH}|g" "$file"
# done
# }
# Upload sourcemaps to Sentry
# @nocheckin
su -c "sentry-cli login --auth-token $SENTRY_AUTH_TOKEN"
su -c "sentry-cli sourcemaps inject --org sourcebot --project backend /app/packages/backend/dist"
su -c "sentry-cli sourcemaps upload --org sourcebot --project backend /app/packages/backend/dist"
# Start the database and wait for it to be ready before starting any other service
if [ "$DATABASE_URL" = "postgresql://postgres@localhost:5432/sourcebot" ]; then
su postgres -c "postgres -D $DB_DATA_DIR" &

View file

@ -1 +0,0 @@
POSTHOG_HOST=https://us.i.posthog.com

View file

@ -32,6 +32,7 @@
"@sourcebot/db": "^0.1.0",
"@sourcebot/error": "^0.1.0",
"@sourcebot/schemas": "^0.1.0",
"@t3-oss/env-core": "^0.12.0",
"@types/express": "^5.0.0",
"argparse": "^2.0.1",
"bullmq": "^5.34.10",
@ -47,6 +48,7 @@
"prom-client": "^15.1.3",
"simple-git": "^3.27.0",
"strip-json-comments": "^5.0.1",
"winston": "^3.15.0"
"winston": "^3.15.0",
"zod": "^3.24.2"
}
}

View file

@ -0,0 +1,41 @@
import { createEnv } from "@t3-oss/env-core";
import { z } from "zod";
import dotenv from 'dotenv';
dotenv.config({
path: './.env',
});
dotenv.config({
path: './.env.local',
override: true
});
export const env = createEnv({
server: {
SOURCEBOT_ENCRYPTION_KEY: z.string(),
SOURCEBOT_LOG_LEVEL: z.enum(["info", "debug", "warn", "error"]).default("info"),
SOURCEBOT_TELEMETRY_DISABLED: z.enum(["true", "false"]).default("false"),
SOURCEBOT_INSTALL_ID: z.string().default("unknown"),
SOURCEBOT_VERSION: z.string().default("unknown"),
POSTHOG_PAPIK: z.string().optional(),
POSTHOG_HOST: z.string().url().default('https://us.i.posthog.com'),
FALLBACK_GITHUB_TOKEN: z.string().optional(),
FALLBACK_GITLAB_TOKEN: z.string().optional(),
FALLBACK_GITEA_TOKEN: z.string().optional(),
REDIS_URL: z.string().url().optional().default("redis://localhost:6379"),
SENTRY_BACKEND_DSN: z.string().optional(),
SENTRY_ENVIRONMENT: z.string().optional(),
LOGTAIL_TOKEN: z.string().optional(),
LOGTAIL_HOST: z.string().url().optional(),
},
runtimeEnv: process.env,
emptyStringAsUndefined: true,
// Skip environment variable validation in Docker builds.
skipValidation: process.env.DOCKER_BUILD === "1",
});

View file

@ -1,48 +0,0 @@
import dotenv from 'dotenv';
import * as Sentry from "@sentry/node";
export const getEnv = (env: string | undefined, defaultValue?: string, required?: boolean) => {
if (required && !env && !defaultValue) {
const e = new Error(`Missing required environment variable: ${env}`);
Sentry.captureException(e);
throw e;
}
return env ?? defaultValue;
}
export const getEnvBoolean = (env: string | undefined, defaultValue: boolean) => {
if (!env) {
return defaultValue;
}
return env === 'true' || env === '1';
}
dotenv.config({
path: './.env',
});
dotenv.config({
path: './.env.local',
override: true
});
export const SOURCEBOT_LOG_LEVEL = getEnv(process.env.SOURCEBOT_LOG_LEVEL, 'info')!;
export const SOURCEBOT_TELEMETRY_DISABLED = getEnvBoolean(process.env.SOURCEBOT_TELEMETRY_DISABLED, false)!;
export const SOURCEBOT_INSTALL_ID = getEnv(process.env.SOURCEBOT_INSTALL_ID, 'unknown')!;
export const SOURCEBOT_VERSION = getEnv(process.env.SOURCEBOT_VERSION, 'unknown')!;
export const POSTHOG_PAPIK = getEnv(process.env.POSTHOG_PAPIK);
export const POSTHOG_HOST = getEnv(process.env.POSTHOG_HOST);
export const FALLBACK_GITHUB_TOKEN = getEnv(process.env.FALLBACK_GITHUB_TOKEN);
export const FALLBACK_GITLAB_TOKEN = getEnv(process.env.FALLBACK_GITLAB_TOKEN);
export const FALLBACK_GITEA_TOKEN = getEnv(process.env.FALLBACK_GITEA_TOKEN);
export const INDEX_CONCURRENCY_MULTIPLE = getEnv(process.env.INDEX_CONCURRENCY_MULTIPLE);
export const REDIS_URL = getEnv(process.env.REDIS_URL, 'redis://localhost:6379')!;
export const SENTRY_BACKEND_DSN = getEnv(process.env.SENTRY_BACKEND_DSN);
export const SENTRY_ENVIRONMENT = getEnv(process.env.SENTRY_ENVIRONMENT, 'unknown')!;
export const LOGTAIL_TOKEN = getEnv(process.env.LOGTAIL_TOKEN);
export const LOGTAIL_HOST = getEnv(process.env.LOGTAIL_HOST);

View file

@ -5,15 +5,15 @@ import fetch from 'cross-fetch';
import { createLogger } from './logger.js';
import micromatch from 'micromatch';
import { PrismaClient } from '@sourcebot/db';
import { FALLBACK_GITEA_TOKEN } from './environment.js';
import { processPromiseResults, throwIfAnyFailed } from './connectionUtils.js';
import * as Sentry from "@sentry/node";
import { env } from './env.js';
const logger = createLogger('Gitea');
export const getGiteaReposFromConfig = async (config: GiteaConnectionConfig, orgId: number, db: PrismaClient) => {
const tokenResult = config.token ? await getTokenFromConfig(config.token, orgId, db) : undefined;
const token = tokenResult?.token ?? FALLBACK_GITEA_TOKEN;
const token = tokenResult?.token ?? env.FALLBACK_GITEA_TOKEN;
const api = giteaApi(config.url ?? 'https://gitea.com', {
token: token,

View file

@ -4,10 +4,10 @@ import { createLogger } from "./logger.js";
import { getTokenFromConfig, measure, fetchWithRetry } from "./utils.js";
import micromatch from "micromatch";
import { PrismaClient } from "@sourcebot/db";
import { FALLBACK_GITHUB_TOKEN } from "./environment.js";
import { BackendException, BackendError } from "@sourcebot/error";
import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
import * as Sentry from "@sentry/node";
import { env } from "./env.js";
const logger = createLogger("GitHub");
@ -45,7 +45,7 @@ export const getGitHubReposFromConfig = async (config: GithubConnectionConfig, o
const secretKey = tokenResult?.secretKey;
const octokit = new Octokit({
auth: token ?? FALLBACK_GITHUB_TOKEN,
auth: token ?? env.FALLBACK_GITHUB_TOKEN,
...(config.url ? {
baseUrl: `${config.url}/api/v3`
} : {}),

View file

@ -4,16 +4,16 @@ import { createLogger } from "./logger.js";
import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type"
import { getTokenFromConfig, measure, fetchWithRetry } from "./utils.js";
import { PrismaClient } from "@sourcebot/db";
import { FALLBACK_GITLAB_TOKEN } from "./environment.js";
import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
import * as Sentry from "@sentry/node";
import { env } from "./env.js";
const logger = createLogger("GitLab");
export const GITLAB_CLOUD_HOSTNAME = "gitlab.com";
export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig, orgId: number, db: PrismaClient) => {
const tokenResult = config.token ? await getTokenFromConfig(config.token, orgId, db) : undefined;
const token = tokenResult?.token ?? FALLBACK_GITLAB_TOKEN;
const token = tokenResult?.token ?? env.FALLBACK_GITLAB_TOKEN;
const api = new Gitlab({
...(token ? {

View file

@ -1,8 +1,8 @@
import * as Sentry from "@sentry/node";
import { SOURCEBOT_VERSION, SENTRY_BACKEND_DSN, SENTRY_ENVIRONMENT } from "./environment.js";
import { env } from "./env.js";
Sentry.init({
dsn: SENTRY_BACKEND_DSN,
release: SOURCEBOT_VERSION,
environment: SENTRY_ENVIRONMENT,
dsn: env.SENTRY_BACKEND_DSN,
release: env.SOURCEBOT_VERSION,
environment: env.SENTRY_ENVIRONMENT,
});

View file

@ -1,14 +1,14 @@
import winston, { format } from 'winston';
import { SOURCEBOT_LOG_LEVEL, LOGTAIL_TOKEN, LOGTAIL_HOST } from './environment.js';
import { Logtail } from '@logtail/node';
import { LogtailTransport } from '@logtail/winston';
import { env } from './env.js';
const { combine, colorize, timestamp, prettyPrint, errors, printf, label: labelFn } = format;
const createLogger = (label: string) => {
return winston.createLogger({
level: SOURCEBOT_LOG_LEVEL,
level: env.SOURCEBOT_LOG_LEVEL,
format: combine(
errors({ stack: true }),
timestamp(),
@ -31,10 +31,10 @@ const createLogger = (label: string) => {
}),
),
}),
...(LOGTAIL_TOKEN && LOGTAIL_HOST ? [
...(env.LOGTAIL_TOKEN && env.LOGTAIL_HOST ? [
new LogtailTransport(
new Logtail(LOGTAIL_TOKEN, {
endpoint: LOGTAIL_HOST,
new Logtail(env.LOGTAIL_TOKEN, {
endpoint: env.LOGTAIL_HOST,
})
)
] : []),

View file

@ -5,13 +5,13 @@ import { DEFAULT_SETTINGS } from './constants.js';
import { Redis } from 'ioredis';
import { ConnectionManager } from './connectionManager.js';
import { RepoManager } from './repoManager.js';
import { INDEX_CONCURRENCY_MULTIPLE, REDIS_URL } from './environment.js';
import { env } from './env.js';
import { PromClient } from './promClient.js';
const logger = createLogger('main');
export const main = async (db: PrismaClient, context: AppContext) => {
const redis = new Redis(REDIS_URL, {
const redis = new Redis(env.REDIS_URL, {
maxRetriesPerRequest: null
});
redis.ping().then(() => {
@ -23,8 +23,8 @@ export const main = async (db: PrismaClient, context: AppContext) => {
});
const settings = DEFAULT_SETTINGS;
if (INDEX_CONCURRENCY_MULTIPLE) {
settings.indexConcurrencyMultiple = parseInt(INDEX_CONCURRENCY_MULTIPLE);
if (env.INDEX_CONCURRENCY_MULTIPLE) {
settings.indexConcurrencyMultiple = env.INDEX_CONCURRENCY_MULTIPLE;
}
const promClient = new PromClient();

View file

@ -1,29 +1,29 @@
import { PostHog } from 'posthog-node';
import { PosthogEvent, PosthogEventMap } from './posthogEvents.js';
import { POSTHOG_HOST, POSTHOG_PAPIK, SOURCEBOT_INSTALL_ID, SOURCEBOT_TELEMETRY_DISABLED, SOURCEBOT_VERSION } from './environment.js';
import { env } from './env.js';
let posthog: PostHog | undefined = undefined;
if (POSTHOG_PAPIK) {
if (env.POSTHOG_PAPIK) {
posthog = new PostHog(
POSTHOG_PAPIK,
env.POSTHOG_PAPIK,
{
host: POSTHOG_HOST,
host: env.POSTHOG_HOST,
}
);
}
export function captureEvent<E extends PosthogEvent>(event: E, properties: PosthogEventMap[E]) {
if (SOURCEBOT_TELEMETRY_DISABLED) {
if (env.SOURCEBOT_TELEMETRY_DISABLED) {
return;
}
posthog?.capture({
distinctId: SOURCEBOT_INSTALL_ID,
distinctId: env.SOURCEBOT_INSTALL_ID,
event: event,
properties: {
...properties,
sourcebot_version: SOURCEBOT_VERSION,
sourcebot_version: env.SOURCEBOT_VERSION,
},
});
}

View file

@ -1,3 +0,0 @@
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
NEXT_PUBLIC_POSTHOG_ASSET_HOST=https://us-assets.i.posthog.com
NEXT_PUBLIC_POSTHOG_UI_HOST=https://us.posthog.com

View file

@ -1,22 +1,30 @@
import {withSentryConfig} from "@sentry/nextjs";
await import("./src/env.mjs");
import { withSentryConfig } from "@sentry/nextjs";
import { env } from "./src/env.mjs";
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
// This is required when using standalone builds.
// @see: https://env.t3.gg/docs/nextjs#create-your-schema
transpilePackages: ["@t3-oss/env-nextjs", "@t3-oss/env-core"],
// @see : https://posthog.com/docs/advanced/proxy/nextjs
async rewrites() {
return [
{
source: "/ingest/static/:path*",
destination: `${process.env.NEXT_PUBLIC_POSTHOG_ASSET_HOST}/static/:path*`,
destination: `${env.NEXT_PUBLIC_POSTHOG_ASSET_HOST}/static/:path*`,
},
{
source: "/ingest/:path*",
destination: `${process.env.NEXT_PUBLIC_POSTHOG_HOST}/:path*`,
destination: `${env.NEXT_PUBLIC_POSTHOG_HOST}/:path*`,
},
{
source: "/ingest/decide",
destination: `${process.env.NEXT_PUBLIC_POSTHOG_HOST}/decide`,
destination: `${env.NEXT_PUBLIC_POSTHOG_HOST}/decide`,
},
];
},
@ -30,51 +38,42 @@ const nextConfig = {
hostname: '**',
},
]
}
// @nocheckin: This was interfering with the the `matcher` regex in middleware.ts,
// causing regular expressions parsing errors when making a request. It's unclear
// why exactly this was happening, but it's likely due to a bad replacement happening
// in the `sed` command.
// @note: this is evaluated at build time.
// ...(process.env.NEXT_PUBLIC_DOMAIN_SUB_PATH ? {
// basePath: process.env.NEXT_PUBLIC_DOMAIN_SUB_PATH,
// } : {})
},
};
export default withSentryConfig(nextConfig, {
// For all available options, see:
// https://www.npmjs.com/package/@sentry/webpack-plugin#options
// For all available options, see:
// https://www.npmjs.com/package/@sentry/webpack-plugin#options
org: "sourcebot",
project: "webapp",
org: "sourcebot",
project: "webapp",
// Only print logs for uploading source maps in CI
silent: !process.env.CI,
// Only print logs for uploading source maps in CI
silent: !process.env.CI,
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Automatically annotate React components to show their full name in breadcrumbs and session replay
reactComponentAnnotation: {
enabled: true,
},
// Automatically annotate React components to show their full name in breadcrumbs and session replay
reactComponentAnnotation: {
enabled: true,
},
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
tunnelRoute: "/monitoring",
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
tunnelRoute: "/monitoring",
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
// See the following for more information:
// https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs
automaticVercelMonitors: true,
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
// See the following for more information:
// https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs
automaticVercelMonitors: true,
});

View file

@ -75,6 +75,7 @@
"@ssddanbrown/codemirror-lang-twig": "^1.0.0",
"@stripe/react-stripe-js": "^3.1.1",
"@stripe/stripe-js": "^5.6.0",
"@t3-oss/env-nextjs": "^0.12.0",
"@tanstack/react-query": "^5.53.3",
"@tanstack/react-table": "^8.20.5",
"@tanstack/react-virtual": "^3.10.8",
@ -133,7 +134,7 @@
"tailwind-scrollbar-hide": "^1.1.7",
"tailwindcss-animate": "^1.0.7",
"usehooks-ts": "^3.1.0",
"zod": "^3.23.8"
"zod": "^3.24.2"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",

View file

@ -16,10 +16,9 @@ import { decrypt, encrypt } from "@sourcebot/crypto"
import { getConnection } from "./data/connection";
import { ConnectionSyncStatus, Prisma, OrgRole, RepoIndexingStatus, StripeSubscriptionStatus } from "@sourcebot/db";
import { cookies, headers } from "next/headers"
import { getStripe } from "@/lib/stripe"
import { getUser } from "@/data/user";
import { Session } from "next-auth";
import { STRIPE_PRODUCT_ID, CONFIG_MAX_REPOS_NO_TOKEN, EMAIL_FROM, SMTP_CONNECTION_URL, AUTH_URL } from "@/lib/environment";
import { env } from "@/env.mjs";
import Stripe from "stripe";
import { render } from "@react-email/components";
import InviteUserEmail from "./emails/inviteUserEmail";
@ -27,6 +26,7 @@ import { createTransport } from "nodemailer";
import { orgDomainSchema, orgNameSchema, repositoryQuerySchema } from "./lib/schemas";
import { RepositoryQuery } from "./lib/types";
import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME } from "./lib/constants";
import { stripeClient } from "./lib/stripe";
const ajv = new Ajv({
validateFormats: false,
@ -594,7 +594,7 @@ export const createInvites = async (emails: string[], domain: string): Promise<{
});
// Send invites to recipients
if (SMTP_CONNECTION_URL && EMAIL_FROM) {
if (env.SMTP_CONNECTION_URL && env.EMAIL_FROM) {
const origin = (await headers()).get('origin')!;
await Promise.all(emails.map(async (email) => {
const invite = await prisma.invite.findUnique({
@ -619,9 +619,9 @@ export const createInvites = async (emails: string[], domain: string): Promise<{
},
});
const inviteLink = `${origin}/redeem?invite_id=${invite.id}`;
const transport = createTransport(SMTP_CONNECTION_URL);
const transport = createTransport(env.SMTP_CONNECTION_URL);
const html = await render(InviteUserEmail({
baseUrl: AUTH_URL,
baseUrl: env.AUTH_URL,
host: {
name: session.user.name ?? undefined,
email: session.user.email!,
@ -637,7 +637,7 @@ export const createInvites = async (emails: string[], domain: string): Promise<{
const result = await transport.sendMail({
to: email,
from: EMAIL_FROM,
from: env.EMAIL_FROM,
subject: `Join ${invite.org.name} on Sourcebot`,
html,
text: `Join ${invite.org.name} on Sourcebot by clicking here: ${inviteLink}`,
@ -718,8 +718,7 @@ export const redeemInvite = async (inviteId: string): Promise<{ success: boolean
const existingSeatCount = subscription.items.data[0].quantity;
const newSeatCount = (existingSeatCount || 1) + 1
const stripe = getStripe();
await stripe.subscriptionItems.update(
await stripeClient?.subscriptionItems.update(
subscription.items.data[0].id,
{
quantity: newSeatCount,
@ -873,10 +872,16 @@ export const createOnboardingSubscription = async (domain: string) =>
return notFound();
}
const stripe = getStripe();
if (!stripeClient) {
return {
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
errorCode: ErrorCode.STRIPE_CLIENT_NOT_INITIALIZED,
message: "Stripe client is not initialized.",
} satisfies ServiceError;
}
// @nocheckin
const test_clock = AUTH_URL !== "https://app.sourcebot.dev" ? await stripe.testHelpers.testClocks.create({
const test_clock = env.AUTH_URL !== "https://app.sourcebot.dev" ? await stripeClient.testHelpers.testClocks.create({
frozen_time: Math.floor(Date.now() / 1000)
}) : null;
@ -886,7 +891,7 @@ export const createOnboardingSubscription = async (domain: string) =>
return org.stripeCustomerId;
}
const customer = await stripe.customers.create({
const customer = await stripeClient.customers.create({
name: org.name,
email: user.email ?? undefined,
test_clock: test_clock?.id,
@ -915,13 +920,13 @@ export const createOnboardingSubscription = async (domain: string) =>
}
const prices = await stripe.prices.list({
product: STRIPE_PRODUCT_ID,
const prices = await stripeClient.prices.list({
product: env.STRIPE_PRODUCT_ID,
expand: ['data.product'],
});
try {
const subscription = await stripe.subscriptions.create({
const subscription = await stripeClient.subscriptions.create({
customer: customerId,
items: [{
price: prices.data[0].id,
@ -974,6 +979,14 @@ export const createStripeCheckoutSession = async (domain: string) =>
return notFound();
}
if (!stripeClient) {
return {
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
errorCode: ErrorCode.STRIPE_CLIENT_NOT_INITIALIZED,
message: "Stripe client is not initialized.",
} satisfies ServiceError;
}
const orgMembers = await prisma.userToOrg.findMany({
where: {
orgId,
@ -984,14 +997,13 @@ export const createStripeCheckoutSession = async (domain: string) =>
});
const numOrgMembers = orgMembers.length;
const stripe = getStripe();
const origin = (await headers()).get('origin')
const prices = await stripe.prices.list({
product: STRIPE_PRODUCT_ID,
const prices = await stripeClient.prices.list({
product: env.STRIPE_PRODUCT_ID,
expand: ['data.product'],
});
const stripeSession = await stripe.checkout.sessions.create({
const stripeSession = await stripeClient.checkout.sessions.create({
customer: org.stripeCustomerId as string,
payment_method_types: ['card'],
line_items: [
@ -1033,9 +1045,16 @@ export const getCustomerPortalSessionLink = async (domain: string): Promise<stri
return notFound();
}
const stripe = getStripe();
if (!stripeClient) {
return {
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
errorCode: ErrorCode.STRIPE_CLIENT_NOT_INITIALIZED,
message: "Stripe client is not initialized.",
} satisfies ServiceError;
}
const origin = (await headers()).get('origin')
const portalSession = await stripe.billingPortal.sessions.create({
const portalSession = await stripeClient.billingPortal.sessions.create({
customer: org.stripeCustomerId as string,
return_url: `${origin}/${domain}/settings/billing`,
});
@ -1064,8 +1083,15 @@ export const getSubscriptionBillingEmail = async (domain: string): Promise<strin
return notFound();
}
const stripe = getStripe();
const customer = await stripe.customers.retrieve(org.stripeCustomerId);
if (!stripeClient) {
return {
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
errorCode: ErrorCode.STRIPE_CLIENT_NOT_INITIALIZED,
message: "Stripe client is not initialized.",
} satisfies ServiceError;
}
const customer = await stripeClient.customers.retrieve(org.stripeCustomerId);
if (!('email' in customer) || customer.deleted) {
return notFound();
}
@ -1086,8 +1112,15 @@ export const changeSubscriptionBillingEmail = async (domain: string, newEmail: s
return notFound();
}
const stripe = getStripe();
await stripe.customers.update(org.stripeCustomerId, {
if (!stripeClient) {
return {
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
errorCode: ErrorCode.STRIPE_CLIENT_NOT_INITIALIZED,
message: "Stripe client is not initialized.",
} satisfies ServiceError;
}
await stripeClient.customers.update(org.stripeCustomerId, {
email: newEmail,
});
@ -1143,8 +1176,7 @@ export const removeMemberFromOrg = async (memberId: string, domain: string): Pro
const existingSeatCount = subscription.items.data[0].quantity;
const newSeatCount = (existingSeatCount || 1) - 1;
const stripe = getStripe();
await stripe.subscriptionItems.update(
await stripeClient?.subscriptionItems.update(
subscription.items.data[0].id,
{
quantity: newSeatCount,
@ -1198,8 +1230,7 @@ export const leaveOrg = async (domain: string): Promise<{ success: boolean } | S
const existingSeatCount = subscription.items.data[0].quantity;
const newSeatCount = (existingSeatCount || 1) - 1;
const stripe = getStripe();
await stripe.subscriptionItems.update(
await stripeClient?.subscriptionItems.update(
subscription.items.data[0].id,
{
quantity: newSeatCount,
@ -1307,8 +1338,15 @@ const _fetchSubscriptionForOrg = async (orgId: number, prisma: Prisma.Transactio
return notFound();
}
const stripe = getStripe();
const subscriptions = await stripe.subscriptions.list({
if (!stripeClient) {
return {
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
errorCode: ErrorCode.STRIPE_CLIENT_NOT_INITIALIZED,
message: "Stripe client is not initialized.",
} satisfies ServiceError;
}
const subscriptions = await stripeClient.subscriptions.list({
customer: org.stripeCustomerId
});
@ -1389,11 +1427,11 @@ const parseConnectionConfig = (connectionType: string, config: string) => {
}
})();
if (!hasToken && numRepos && numRepos > CONFIG_MAX_REPOS_NO_TOKEN) {
if (!hasToken && numRepos && numRepos > env.CONFIG_MAX_REPOS_NO_TOKEN) {
return {
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.INVALID_REQUEST_BODY,
message: `You must provide a token to sync more than ${CONFIG_MAX_REPOS_NO_TOKEN} repositories.`,
message: `You must provide a token to sync more than ${env.CONFIG_MAX_REPOS_NO_TOKEN} repositories.`,
} satisfies ServiceError;
}

View file

@ -6,7 +6,7 @@ import { CircleXIcon } from "lucide-react";
import { useDomain } from "@/hooks/useDomain";
import { unwrapServiceError } from "@/lib/utils";
import useCaptureEvent from "@/hooks/useCaptureEvent";
import { NEXT_PUBLIC_POLLING_INTERVAL_MS } from "@/lib/environment.client";
import { env } from "@/env.mjs";
import { useQuery } from "@tanstack/react-query";
import { ConnectionSyncStatus, RepoIndexingStatus } from "@sourcebot/db";
import { getConnections } from "@/actions";
@ -20,14 +20,14 @@ export const ErrorNavIndicator = () => {
queryKey: ['repos', domain],
queryFn: () => unwrapServiceError(getRepos(domain)),
select: (data) => data.filter(repo => repo.repoIndexingStatus === RepoIndexingStatus.FAILED),
refetchInterval: NEXT_PUBLIC_POLLING_INTERVAL_MS,
refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
});
const { data: connections, isPending: isPendingConnections, isError: isErrorConnections } = useQuery({
queryKey: ['connections', domain],
queryFn: () => unwrapServiceError(getConnections(domain)),
select: (data) => data.filter(connection => connection.syncStatus === ConnectionSyncStatus.FAILED),
refetchInterval: NEXT_PUBLIC_POLLING_INTERVAL_MS,
refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
});
if (isPendingRepos || isErrorRepos || isPendingConnections || isErrorConnections) {

View file

@ -4,7 +4,7 @@ import { getRepos } from "@/actions";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
import useCaptureEvent from "@/hooks/useCaptureEvent";
import { useDomain } from "@/hooks/useDomain";
import { NEXT_PUBLIC_POLLING_INTERVAL_MS } from "@/lib/environment.client";
import { env } from "@/env.mjs";
import { unwrapServiceError } from "@/lib/utils";
import { RepoIndexingStatus } from "@prisma/client";
import { useQuery } from "@tanstack/react-query";
@ -19,7 +19,7 @@ export const ProgressNavIndicator = () => {
queryKey: ['repos', domain],
queryFn: () => unwrapServiceError(getRepos(domain)),
select: (data) => data.filter(repo => repo.repoIndexingStatus === RepoIndexingStatus.IN_INDEX_QUEUE || repo.repoIndexingStatus === RepoIndexingStatus.INDEXING),
refetchInterval: NEXT_PUBLIC_POLLING_INTERVAL_MS,
refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
});
if (isPending || isError || inProgressRepos.length === 0) {

View file

@ -6,7 +6,7 @@ import { useDomain } from "@/hooks/useDomain";
import { useQuery } from "@tanstack/react-query";
import { unwrapServiceError } from "@/lib/utils";
import { getRepos } from "@/actions";
import { NEXT_PUBLIC_POLLING_INTERVAL_MS } from "@/lib/environment.client";
import { env } from "@/env.mjs";
import { Skeleton } from "@/components/ui/skeleton";
import {
Carousel,
@ -22,7 +22,7 @@ export function RepositorySnapshot() {
const { data: repos, isPending, isError } = useQuery({
queryKey: ['repos', domain],
queryFn: () => unwrapServiceError(getRepos(domain)),
refetchInterval: NEXT_PUBLIC_POLLING_INTERVAL_MS,
refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
});
if (isPending || isError || !repos) {

View file

@ -28,11 +28,10 @@ import { useMemo } from "react"
import { KeymapType } from "@/lib/types"
import { cn } from "@/lib/utils"
import { useKeymapType } from "@/hooks/useKeymapType"
import { NEXT_PUBLIC_SOURCEBOT_VERSION } from "@/lib/environment.client";
import { useSession } from "next-auth/react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { signOut } from "next-auth/react"
import { env } from "@/env.mjs";
interface SettingsDropdownProps {
menuButtonClassName?: string;
@ -147,7 +146,7 @@ export const SettingsDropdown = ({
</DropdownMenuGroup>
<DropdownMenuSeparator />
<div className="px-2 py-1 text-sm text-muted-foreground">
version: {NEXT_PUBLIC_SOURCEBOT_VERSION}
version: {env.NEXT_PUBLIC_SOURCEBOT_VERSION}
</div>
</DropdownMenuContent>
</DropdownMenu>

View file

@ -7,7 +7,7 @@ import { useDomain } from "@/hooks/useDomain";
import { getConnections } from "@/actions";
import { unwrapServiceError } from "@/lib/utils";
import useCaptureEvent from "@/hooks/useCaptureEvent";
import { NEXT_PUBLIC_POLLING_INTERVAL_MS } from "@/lib/environment.client";
import { env } from "@/env.mjs";
import { useQuery } from "@tanstack/react-query";
import { ConnectionSyncStatus } from "@prisma/client";
@ -19,7 +19,7 @@ export const WarningNavIndicator = () => {
queryKey: ['connections', domain],
queryFn: () => unwrapServiceError(getConnections(domain)),
select: (data) => data.filter(connection => connection.syncStatus === ConnectionSyncStatus.SYNCED_WITH_WARNINGS),
refetchInterval: NEXT_PUBLIC_POLLING_INTERVAL_MS,
refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
});
if (isPending || isError || connections.length === 0) {

View file

@ -10,7 +10,7 @@ import { useCallback } from "react";
import { useQuery } from "@tanstack/react-query";
import { flagConnectionForSync, getConnectionInfo } from "@/actions";
import { isServiceError, unwrapServiceError } from "@/lib/utils";
import { NEXT_PUBLIC_POLLING_INTERVAL_MS } from "@/lib/environment.client";
import { env } from "@/env.mjs";
import { ConnectionSyncStatus } from "@sourcebot/db";
import { FiLoader } from "react-icons/fi";
import { CircleCheckIcon, AlertTriangle, CircleXIcon } from "lucide-react";
@ -31,7 +31,7 @@ export const Overview = ({ connectionId }: OverviewProps) => {
const { data: connection, isPending, error, refetch } = useQuery({
queryKey: ['connection', domain, connectionId],
queryFn: () => unwrapServiceError(getConnectionInfo(connectionId, domain)),
refetchInterval: NEXT_PUBLIC_POLLING_INTERVAL_MS,
refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
});
const handleSecretsNavigation = useCallback(() => {

View file

@ -11,7 +11,7 @@ import { Search, Loader2 } from "lucide-react";
import { Input } from "@/components/ui/input";
import { useCallback, useMemo, useState } from "react";
import { RepoListItemSkeleton } from "./repoListItemSkeleton";
import { NEXT_PUBLIC_POLLING_INTERVAL_MS } from "@/lib/environment.client";
import { env } from "@/env.mjs";
import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation";
import { MultiSelect } from "@/components/ui/multi-select";
@ -78,7 +78,7 @@ export const RepoList = ({ connectionId }: RepoListProps) => {
return new Date(a.indexedAt ?? new Date()).getTime() - new Date(b.indexedAt ?? new Date()).getTime();
});
},
refetchInterval: NEXT_PUBLIC_POLLING_INTERVAL_MS,
refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
});
const { data: connection, isPending: isConnectionPending, error: isConnectionError } = useQuery({

View file

@ -6,7 +6,7 @@ import { InfoCircledIcon } from "@radix-ui/react-icons";
import { getConnections } from "@/actions";
import { Skeleton } from "@/components/ui/skeleton";
import { useQuery } from "@tanstack/react-query";
import { NEXT_PUBLIC_POLLING_INTERVAL_MS } from "@/lib/environment.client";
import { env } from "@/env.mjs";
import { RepoIndexingStatus, ConnectionSyncStatus } from "@sourcebot/db";
import { Search } from "lucide-react";
import { Input } from "@/components/ui/input";
@ -43,7 +43,7 @@ export const ConnectionList = ({
const { data: unfilteredConnections, isPending, error } = useQuery({
queryKey: ['connections', domain],
queryFn: () => unwrapServiceError(getConnections(domain)),
refetchInterval: NEXT_PUBLIC_POLLING_INTERVAL_MS,
refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
});
const connections = useMemo(() => {

View file

@ -5,11 +5,11 @@ import { columns, RepositoryColumnInfo } from "./columns";
import { unwrapServiceError } from "@/lib/utils";
import { getRepos } from "@/actions";
import { useQuery } from "@tanstack/react-query";
import { NEXT_PUBLIC_POLLING_INTERVAL_MS } from "@/lib/environment.client";
import { useDomain } from "@/hooks/useDomain";
import { RepoIndexingStatus } from "@sourcebot/db";
import { useMemo } from "react";
import { Skeleton } from "@/components/ui/skeleton";
import { env } from "@/env.mjs";
export const RepositoryTable = () => {
const domain = useDomain();
@ -19,7 +19,7 @@ export const RepositoryTable = () => {
queryFn: async () => {
return await unwrapServiceError(getRepos(domain));
},
refetchInterval: NEXT_PUBLIC_POLLING_INTERVAL_MS,
refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
refetchIntervalInBackground: true,
});

View file

@ -22,7 +22,6 @@ import { CodePreviewPanel } from "./components/codePreviewPanel";
import { FilterPanel } from "./components/filterPanel";
import { SearchResultsPanel } from "./components/searchResultsPanel";
import { useDomain } from "@/hooks/useDomain";
import { NEXT_PUBLIC_PUBLIC_SEARCH_DEMO } from "@/lib/environment.client";
const DEFAULT_MAX_MATCH_DISPLAY_COUNT = 10000;
@ -105,7 +104,6 @@ const SearchPageInternal = () => {
const fileLanguages = searchResponse.Result.Files?.map(file => file.Language) || [];
captureEvent("search_finished", {
query: NEXT_PUBLIC_PUBLIC_SEARCH_DEMO ? searchQuery : null, // @nocheckin
contentBytesLoaded: searchResponse.Result.ContentBytesLoaded,
indexBytesLoaded: searchResponse.Result.IndexBytesLoaded,
crashes: searchResponse.Result.Crashes,

View file

@ -4,7 +4,7 @@ import { isServiceError } from "@/lib/utils";
import { getCurrentUserRole } from "@/actions";
import { getOrgFromDomain } from "@/data/org";
import { ChangeOrgDomainCard } from "./components/changeOrgDomainCard";
import { SOURCEBOT_ROOT_DOMAIN } from "@/lib/environment";
import { env } from "@/env.mjs";
interface GeneralSettingsPageProps {
params: {
@ -42,7 +42,7 @@ export default async function GeneralSettingsPage({ params: { domain } }: Genera
<ChangeOrgDomainCard
orgDomain={org.domain}
currentUserRole={currentUserRole}
rootDomain={SOURCEBOT_ROOT_DOMAIN}
rootDomain={env.SOURCEBOT_ROOT_DOMAIN}
/>
</div>
)

View file

@ -1,7 +1,6 @@
'use client';
import { OrgRole } from "@sourcebot/db";
import { resolveServerPath } from "@/app/api/(client)/client";
import { useToast } from "@/components/hooks/use-toast";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
@ -124,8 +123,7 @@ export const InvitesList = ({ invites, currentUserRole }: InviteListProps) => {
className="gap-2"
title="Copy invite link"
onClick={() => {
const basePath = `${window.location.origin}${resolveServerPath('/')}`;
const url = createPathWithQueryParams(`${basePath}redeem?invite_id=${invite.id}`);
const url = createPathWithQueryParams(`${window.location.origin}/redeem?invite_id=${invite.id}`);
navigator.clipboard.writeText(url)
.then(() => {
toast({

View file

@ -1,13 +1,11 @@
'use client';
import { NEXT_PUBLIC_DOMAIN_SUB_PATH } from "@/lib/environment.client";
import { fileSourceResponseSchema, getVersionResponseSchema, listRepositoriesResponseSchema, searchResponseSchema } from "@/lib/schemas";
import { FileSourceRequest, FileSourceResponse, GetVersionResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "@/lib/types";
import assert from "assert";
export const search = async (body: SearchRequest, domain: string): Promise<SearchResponse> => {
const path = resolveServerPath("/api/search");
const result = await fetch(path, {
const result = await fetch("/api/search", {
method: "POST",
headers: {
"Content-Type": "application/json",
@ -20,8 +18,7 @@ export const search = async (body: SearchRequest, domain: string): Promise<Searc
}
export const fetchFileSource = async (body: FileSourceRequest, domain: string): Promise<FileSourceResponse> => {
const path = resolveServerPath("/api/source");
const result = await fetch(path, {
const result = await fetch("/api/source", {
method: "POST",
headers: {
"Content-Type": "application/json",
@ -34,8 +31,7 @@ export const fetchFileSource = async (body: FileSourceRequest, domain: string):
}
export const getRepos = async (domain: string): Promise<ListRepositoriesResponse> => {
const path = resolveServerPath("/api/repos");
const result = await fetch(path, {
const result = await fetch("/api/repos", {
method: "GET",
headers: {
"Content-Type": "application/json",
@ -47,8 +43,7 @@ export const getRepos = async (domain: string): Promise<ListRepositoriesResponse
}
export const getVersion = async (): Promise<GetVersionResponse> => {
const path = resolveServerPath("/api/version");
const result = await fetch(path, {
const result = await fetch("/api/version", {
method: "GET",
headers: {
"Content-Type": "application/json",
@ -56,13 +51,3 @@ export const getVersion = async (): Promise<GetVersionResponse> => {
}).then(response => response.json());
return getVersionResponseSchema.parse(result);
}
/**
* Given a subpath to a api route on the server (e.g., /api/search),
* returns the full path to that route on the server, taking into account
* the base path (if any).
*/
export const resolveServerPath = (path: string) => {
assert(path.startsWith("/"));
return `${NEXT_PUBLIC_DOMAIN_SUB_PATH}${path}`;
}

View file

@ -2,9 +2,9 @@ import { headers } from 'next/headers';
import { NextRequest } from 'next/server';
import Stripe from 'stripe';
import { prisma } from '@/prisma';
import { STRIPE_WEBHOOK_SECRET } from '@/lib/environment';
import { getStripe } from '@/lib/stripe';
import { ConnectionSyncStatus, StripeSubscriptionStatus } from '@sourcebot/db';
import { stripeClient } from '@/lib/stripe';
import { env } from '@/env.mjs';
export async function POST(req: NextRequest) {
const body = await req.text();
@ -14,12 +14,19 @@ export async function POST(req: NextRequest) {
return new Response('No signature', { status: 400 });
}
if (!stripeClient) {
return new Response('Stripe client not initialized', { status: 500 });
}
if (!env.STRIPE_WEBHOOK_SECRET) {
return new Response('Stripe webhook secret not set', { status: 500 });
}
try {
const stripe = getStripe();
const event = stripe.webhooks.constructEvent(
const event = stripeClient.webhooks.constructEvent(
body,
signature,
STRIPE_WEBHOOK_SECRET!
env.STRIPE_WEBHOOK_SECRET
);
if (event.type === 'customer.subscription.deleted') {

View file

@ -1,4 +1,4 @@
import { SOURCEBOT_VERSION } from "@/lib/environment";
import { env } from "@/env.mjs";
import { GetVersionResponse } from "@/lib/types";
// Note: In Next.JS 14, GET methods with no params are cached by default at build time.
@ -10,6 +10,6 @@ export const dynamic = "force-dynamic";
export const GET = async () => {
return Response.json({
version: SOURCEBOT_VERSION,
version: env.NEXT_PUBLIC_SOURCEBOT_VERSION,
} satisfies GetVersionResponse);
}

View file

@ -6,6 +6,7 @@ import { PostHogProvider } from "./posthogProvider";
import { Toaster } from "@/components/ui/toaster";
import { TooltipProvider } from "@/components/ui/tooltip";
import { SessionProvider } from "next-auth/react";
import { env } from "@/env.mjs";
export const metadata: Metadata = {
title: "Sourcebot",
@ -26,7 +27,7 @@ export default function RootLayout({
<body>
<Toaster />
<SessionProvider>
<PostHogProvider>
<PostHogProvider disabled={env.SOURCEBOT_TELEMETRY_DISABLED === "true"}>
<ThemeProvider
attribute="class"
defaultTheme="system"

View file

@ -4,7 +4,7 @@ import { redirect } from "next/navigation";
import { OnboardHeader } from "./components/onboardHeader";
import { OnboardingSteps } from "@/lib/constants";
import { LogoutEscapeHatch } from "../components/logoutEscapeHatch";
import { SOURCEBOT_ROOT_DOMAIN } from "@/lib/environment";
import { env } from "@/env.mjs";
export default async function Onboarding() {
const session = await auth();
@ -19,7 +19,7 @@ export default async function Onboarding() {
description="Create a organization for your team to search and share code across your repositories."
step={OnboardingSteps.CreateOrg}
/>
<OrgCreateForm rootDomain={SOURCEBOT_ROOT_DOMAIN} />
<OrgCreateForm rootDomain={env.SOURCEBOT_ROOT_DOMAIN} />
<LogoutEscapeHatch className="absolute top-0 right-0 p-4 sm:p-12" />
</div>
);

View file

@ -1,14 +1,10 @@
'use client'
import { NEXT_PUBLIC_POSTHOG_PAPIK, NEXT_PUBLIC_POSTHOG_UI_HOST, NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED, NEXT_PUBLIC_PUBLIC_SEARCH_DEMO } from '@/lib/environment.client'
import posthog from 'posthog-js'
import { usePostHog } from 'posthog-js/react'
import { PostHogProvider as PHProvider } from 'posthog-js/react'
import { resolveServerPath } from './api/(client)/client'
import { isDefined } from '@/lib/utils'
import { usePathname, useSearchParams } from "next/navigation"
import { useEffect, Suspense } from "react"
const POSTHOG_ENABLED = isDefined(NEXT_PUBLIC_POSTHOG_PAPIK) && !NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED;
import { Suspense, useEffect } from "react"
import { env } from '@/env.mjs'
function PostHogPageView() {
const pathname = usePathname()
@ -30,26 +26,23 @@ function PostHogPageView() {
return null
}
export default function SuspendedPostHogPageView() {
return <Suspense fallback={null}>
<PostHogPageView />
</Suspense>
interface PostHogProviderProps {
children: React.ReactNode
disabled: boolean
}
export function PostHogProvider({ children }: { children: React.ReactNode }) {
export function PostHogProvider({ children, disabled }: PostHogProviderProps) {
useEffect(() => {
if (POSTHOG_ENABLED) {
// @see next.config.mjs for path rewrites to the "/ingest" route.
const posthogHostPath = resolveServerPath('/ingest');
posthog.init(NEXT_PUBLIC_POSTHOG_PAPIK!, {
api_host: posthogHostPath,
ui_host: NEXT_PUBLIC_POSTHOG_UI_HOST,
if (!disabled && env.NEXT_PUBLIC_POSTHOG_PAPIK) {
posthog.init(env.NEXT_PUBLIC_POSTHOG_PAPIK, {
// @see next.config.mjs for path rewrites to the "/ingest" route.
api_host: "/ingest",
ui_host: env.NEXT_PUBLIC_POSTHOG_UI_HOST,
person_profiles: 'identified_only',
capture_pageview: NEXT_PUBLIC_PUBLIC_SEARCH_DEMO, // @nocheckin Disable automatic pageview capture if we're not in public demo mode
capture_pageview: false, // @nocheckin Disable automatic pageview capture if we're not in public demo mode
autocapture: false, // Disable automatic event capture
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sanitize_properties: !NEXT_PUBLIC_PUBLIC_SEARCH_DEMO ? (properties: Record<string, any>, _event: string) => {
sanitize_properties: (properties: Record<string, any>, _event: string) => {
// https://posthog.com/docs/libraries/js#config
if (properties['$current_url']) {
properties['$current_url'] = null;
@ -59,16 +52,18 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
}
return properties;
} : undefined
}
});
} else {
console.log("PostHog telemetry disabled");
}
}, [])
}, [disabled])
return (
<PHProvider client={posthog}>
<SuspendedPostHogPageView />
<Suspense fallback={null}>
<PostHogPageView />
</Suspense>
{children}
</PHProvider>
)

View file

@ -6,17 +6,7 @@ import Credentials from "next-auth/providers/credentials"
import EmailProvider from "next-auth/providers/nodemailer";
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/prisma";
import {
AUTH_GITHUB_CLIENT_ID,
AUTH_GITHUB_CLIENT_SECRET,
AUTH_GOOGLE_CLIENT_ID,
AUTH_GOOGLE_CLIENT_SECRET,
AUTH_SECRET,
AUTH_URL,
AUTH_CREDENTIALS_LOGIN_ENABLED,
EMAIL_FROM,
SMTP_CONNECTION_URL
} from "./lib/environment";
import { env } from "@/env.mjs";
import { User } from '@sourcebot/db';
import 'next-auth/jwt';
import type { Provider } from "next-auth/providers";
@ -44,24 +34,24 @@ declare module 'next-auth/jwt' {
export const getProviders = () => {
const providers: Provider[] = [];
if (AUTH_GITHUB_CLIENT_ID && AUTH_GITHUB_CLIENT_SECRET) {
if (env.AUTH_GITHUB_CLIENT_ID && env.AUTH_GITHUB_CLIENT_SECRET) {
providers.push(GitHub({
clientId: AUTH_GITHUB_CLIENT_ID,
clientSecret: AUTH_GITHUB_CLIENT_SECRET,
clientId: env.AUTH_GITHUB_CLIENT_ID,
clientSecret: env.AUTH_GITHUB_CLIENT_SECRET,
}));
}
if (AUTH_GOOGLE_CLIENT_ID && AUTH_GOOGLE_CLIENT_SECRET) {
if (env.AUTH_GOOGLE_CLIENT_ID && env.AUTH_GOOGLE_CLIENT_SECRET) {
providers.push(Google({
clientId: AUTH_GOOGLE_CLIENT_ID,
clientSecret: AUTH_GOOGLE_CLIENT_SECRET,
clientId: env.AUTH_GOOGLE_CLIENT_ID,
clientSecret: env.AUTH_GOOGLE_CLIENT_SECRET,
}));
}
if (SMTP_CONNECTION_URL && EMAIL_FROM) {
if (env.SMTP_CONNECTION_URL && env.EMAIL_FROM) {
providers.push(EmailProvider({
server: SMTP_CONNECTION_URL,
from: EMAIL_FROM,
server: env.SMTP_CONNECTION_URL,
from: env.EMAIL_FROM,
maxAge: 60 * 10,
generateVerificationToken: async () => {
const token = String(Math.floor(100000 + Math.random() * 900000));
@ -69,7 +59,7 @@ export const getProviders = () => {
},
sendVerificationRequest: async ({ identifier, provider, token }) => {
const transport = createTransport(provider.server);
const html = await render(MagicLinkEmail({ baseUrl: AUTH_URL, token: token }));
const html = await render(MagicLinkEmail({ baseUrl: env.AUTH_URL, token: token }));
const result = await transport.sendMail({
to: identifier,
from: provider.from,
@ -86,7 +76,7 @@ export const getProviders = () => {
}));
}
if (AUTH_CREDENTIALS_LOGIN_ENABLED) {
if (env.AUTH_CREDENTIALS_LOGIN_ENABLED) {
providers.push(Credentials({
credentials: {
email: {},
@ -102,7 +92,7 @@ export const getProviders = () => {
// authorize runs in the edge runtime (where we cannot make DB calls / access environment variables),
// so we need to make a request to the server to verify the credentials.
const response = await fetch(new URL('/api/auth/verifyCredentials', AUTH_URL), {
const response = await fetch(new URL('/api/auth/verifyCredentials', env.AUTH_URL), {
method: 'POST',
body: JSON.stringify({ email, password }),
});
@ -125,11 +115,11 @@ export const getProviders = () => {
return providers;
}
const useSecureCookies = AUTH_URL?.startsWith("https://") ?? false;
const hostName = AUTH_URL ? new URL(AUTH_URL).hostname : "localhost";
const useSecureCookies = env.AUTH_URL?.startsWith("https://") ?? false;
const hostName = env.AUTH_URL ? new URL(env.AUTH_URL).hostname : "localhost";
export const { handlers, signIn, signOut, auth } = NextAuth({
secret: AUTH_SECRET,
secret: env.AUTH_SECRET,
adapter: PrismaAdapter(prisma),
session: {
strategy: "jwt",

60
packages/web/src/env.mjs Normal file
View file

@ -0,0 +1,60 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
server: {
// Zoekt
ZOEKT_WEBSERVER_URL: z.string().url().default('http://localhost:6070'),
SHARD_MAX_MATCH_COUNT: z.number().default(10000),
TOTAL_MAX_MATCH_COUNT: z.number().default(100000),
// Auth
AUTH_SECRET: z.string(),
AUTH_GITHUB_CLIENT_ID: z.string().optional(),
AUTH_GITHUB_CLIENT_SECRET: z.string().optional(),
AUTH_GOOGLE_CLIENT_ID: z.string().optional(),
AUTH_GOOGLE_CLIENT_SECRET: z.string().optional(),
AUTH_URL: z.string().url(),
AUTH_CREDENTIALS_LOGIN_ENABLED: z.boolean().default(true),
// Email
SMTP_CONNECTION_URL: z.string().url().optional(),
EMAIL_FROM: z.string().email().optional(),
// Stripe
STRIPE_SECRET_KEY: z.string().optional(),
STRIPE_PRODUCT_ID: z.string().optional(),
STRIPE_WEBHOOK_SECRET: z.string().optional(),
// Misc
CONFIG_MAX_REPOS_NO_TOKEN: z.number().default(500),
SOURCEBOT_ROOT_DOMAIN: z.string().default("localhost:3000"),
NODE_ENV: z.enum(["development", "test", "production"]),
SOURCEBOT_TELEMETRY_DISABLED: z.enum(["true", "false"]).default("false"),
},
// @NOTE: Make sure you destructure all client variables in the
// `experimental__runtimeEnv` block below.
client: {
// PostHog
NEXT_PUBLIC_POSTHOG_PAPIK: z.string().optional(),
NEXT_PUBLIC_POSTHOG_HOST: z.string().url().default('https://us.i.posthog.com'),
NEXT_PUBLIC_POSTHOG_ASSET_HOST: z.string().url().default('https://us-assets.i.posthog.com'),
NEXT_PUBLIC_POSTHOG_UI_HOST: z.string().url().default('https://us.posthog.com'),
// Misc
NEXT_PUBLIC_SOURCEBOT_VERSION: z.string().default('unknown'),
NEXT_PUBLIC_POLLING_INTERVAL_MS: z.number().default(5000),
},
// For Next.js >= 13.4.4, you only need to destructure client variables:
experimental__runtimeEnv: {
NEXT_PUBLIC_POSTHOG_PAPIK: process.env.NEXT_PUBLIC_POSTHOG_PAPIK,
NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
NEXT_PUBLIC_POSTHOG_ASSET_HOST: process.env.NEXT_PUBLIC_POSTHOG_ASSET_HOST,
NEXT_PUBLIC_POSTHOG_UI_HOST: process.env.NEXT_PUBLIC_POSTHOG_UI_HOST,
NEXT_PUBLIC_SOURCEBOT_VERSION: process.env.NEXT_PUBLIC_SOURCEBOT_VERSION,
NEXT_PUBLIC_POLLING_INTERVAL_MS: process.env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
},
// Skip environment variable validation in Docker builds.
skipValidation: process.env.DOCKER_BUILD === "1",
emptyStringAsUndefined: true,
});

View file

@ -3,7 +3,7 @@
import { CaptureOptions } from "posthog-js";
import posthog from "posthog-js";
import { PosthogEvent, PosthogEventMap } from "../lib/posthogEvents";
import { NEXT_PUBLIC_SOURCEBOT_VERSION } from "@/lib/environment.client";
import { env } from "@/env.mjs";
export function captureEvent<E extends PosthogEvent>(event: E, properties: PosthogEventMap[E], options?: CaptureOptions) {
if(!options) {
@ -12,7 +12,7 @@ export function captureEvent<E extends PosthogEvent>(event: E, properties: Posth
options.send_instantly = true;
posthog.capture(event, {
...properties,
sourcebot_version: NEXT_PUBLIC_SOURCEBOT_VERSION,
sourcebot_version: env.NEXT_PUBLIC_SOURCEBOT_VERSION,
}, options);
}

View file

@ -1,14 +0,0 @@
import 'client-only';
import { getEnv, getEnvBoolean, getEnvNumber } from "./utils";
export const NEXT_PUBLIC_POSTHOG_PAPIK = getEnv(process.env.NEXT_PUBLIC_POSTHOG_PAPIK);
export const NEXT_PUBLIC_POSTHOG_HOST = getEnv(process.env.NEXT_PUBLIC_POSTHOG_HOST);
export const NEXT_PUBLIC_POSTHOG_UI_HOST = getEnv(process.env.NEXT_PUBLIC_POSTHOG_UI_HOST);
export const NEXT_PUBLIC_POSTHOG_ASSET_HOST = getEnv(process.env.NEXT_PUBLIC_POSTHOG_ASSET_HOST);
export const NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED = getEnvBoolean(process.env.NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED, false);
export const NEXT_PUBLIC_SOURCEBOT_VERSION = getEnv(process.env.NEXT_PUBLIC_SOURCEBOT_VERSION, "unknown")!;
export const NEXT_PUBLIC_DOMAIN_SUB_PATH = getEnv(process.env.NEXT_PUBLIC_DOMAIN_SUB_PATH, "")!;
export const NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY = getEnv(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
export const NEXT_PUBLIC_POLLING_INTERVAL_MS = getEnvNumber(process.env.NEXT_PUBLIC_POLLING_INTERVAL_MS, 5000);
export const NEXT_PUBLIC_PUBLIC_SEARCH_DEMO = getEnvBoolean(process.env.NEXT_PUBLIC_PUBLIC_SEARCH_DEMO, false);

View file

@ -1,29 +0,0 @@
import 'server-only';
import { getEnv, getEnvBoolean, getEnvNumber } from "./utils";
export const ZOEKT_WEBSERVER_URL = getEnv(process.env.ZOEKT_WEBSERVER_URL, "http://localhost:6070")!;
export const SHARD_MAX_MATCH_COUNT = getEnvNumber(process.env.SHARD_MAX_MATCH_COUNT, 10000);
export const TOTAL_MAX_MATCH_COUNT = getEnvNumber(process.env.TOTAL_MAX_MATCH_COUNT, 100000);
export const SOURCEBOT_VERSION = getEnv(process.env.SOURCEBOT_VERSION, 'unknown')!;
export const NODE_ENV = process.env.NODE_ENV;
export const AUTH_SECRET = getEnv(process.env.AUTH_SECRET); // Generate using `npx auth secret`
export const AUTH_GITHUB_CLIENT_ID = getEnv(process.env.AUTH_GITHUB_CLIENT_ID);
export const AUTH_GITHUB_CLIENT_SECRET = getEnv(process.env.AUTH_GITHUB_CLIENT_SECRET);
export const AUTH_GOOGLE_CLIENT_ID = getEnv(process.env.AUTH_GOOGLE_CLIENT_ID);
export const AUTH_GOOGLE_CLIENT_SECRET = getEnv(process.env.AUTH_GOOGLE_CLIENT_SECRET);
export const AUTH_URL = getEnv(process.env.AUTH_URL)!;
export const AUTH_CREDENTIALS_LOGIN_ENABLED = getEnvBoolean(process.env.AUTH_CREDENTIALS_LOGIN_ENABLED, true);
export const STRIPE_SECRET_KEY = getEnv(process.env.STRIPE_SECRET_KEY);
export const STRIPE_PRODUCT_ID = getEnv(process.env.STRIPE_PRODUCT_ID);
export const STRIPE_WEBHOOK_SECRET = getEnv(process.env.STRIPE_WEBHOOK_SECRET);
export const CONFIG_MAX_REPOS_NO_TOKEN = getEnvNumber(process.env.CONFIG_MAX_REPOS_NO_TOKEN, 500);
export const SMTP_CONNECTION_URL = getEnv(process.env.SMTP_CONNECTION_URL);
export const EMAIL_FROM = getEnv(process.env.EMAIL_FROM);
export const SOURCEBOT_ROOT_DOMAIN = getEnv(process.env.SOURCEBOT_ROOT_DOMAIN, "localhost:3000")!;
export const PUBLIC_SEARCH_DEMO = getEnvBoolean(process.env.PUBLIC_SEARCH_DEMO, false);

View file

@ -19,4 +19,5 @@ export enum ErrorCode {
STRIPE_CHECKOUT_ERROR = 'STRIPE_CHECKOUT_ERROR',
SECRET_ALREADY_EXISTS = 'SECRET_ALREADY_EXISTS',
SUBSCRIPTION_ALREADY_EXISTS = 'SUBSCRIPTION_ALREADY_EXISTS',
STRIPE_CLIENT_NOT_INITIALIZED = 'STRIPE_CLIENT_NOT_INITIALIZED',
}

View file

@ -2,7 +2,6 @@
export type PosthogEventMap = {
search_finished: {
query: string | null,
contentBytesLoaded: number,
indexBytesLoaded: number,
crashes: number,

View file

@ -1,5 +1,5 @@
import escapeStringRegexp from "escape-string-regexp";
import { SHARD_MAX_MATCH_COUNT, TOTAL_MAX_MATCH_COUNT } from "../environment";
import { env } from "@/env.mjs";
import { listRepositoriesResponseSchema, zoektSearchResponseSchema } from "../schemas";
import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "../types";
import { fileNotFound, invalidZoektResponse, ServiceError, unexpectedError } from "../serviceError";
@ -59,8 +59,8 @@ export const search = async ({ query, maxMatchDisplayCount, whole}: SearchReques
ChunkMatches: true,
MaxMatchDisplayCount: maxMatchDisplayCount,
Whole: !!whole,
ShardMaxMatchCount: SHARD_MAX_MATCH_COUNT,
TotalMaxMatchCount: TOTAL_MAX_MATCH_COUNT,
ShardMaxMatchCount: env.SHARD_MAX_MATCH_COUNT,
TotalMaxMatchCount: env.TOTAL_MAX_MATCH_COUNT,
}
});

View file

@ -1,5 +1,4 @@
import { ZOEKT_WEBSERVER_URL } from "../environment"
import { env } from "@/env.mjs";
interface ZoektRequest {
path: string,
@ -17,7 +16,7 @@ export const zoektFetch = async ({
cache,
}: ZoektRequest) => {
const response = await fetch(
new URL(path, ZOEKT_WEBSERVER_URL),
new URL(path, env.ZOEKT_WEBSERVER_URL),
{
method,
headers: {

View file

@ -1,12 +1,8 @@
import 'server-only';
import { env } from '@/env.mjs'
import Stripe from "stripe";
import Stripe from 'stripe'
import { STRIPE_SECRET_KEY } from './environment'
let stripeInstance: Stripe | null = null;
export const getStripe = () => {
if (!stripeInstance) {
stripeInstance = new Stripe(STRIPE_SECRET_KEY!);
}
return stripeInstance;
}
export const stripeClient =
env.STRIPE_SECRET_KEY
? new Stripe(env.STRIPE_SECRET_KEY)
: undefined;

View file

@ -158,30 +158,6 @@ export const isServiceError = (data: unknown): data is ServiceError => {
'message' in data;
}
export const getEnv = (env: string | undefined, defaultValue?: string) => {
return env ?? defaultValue;
}
export const getEnvNumber = (env: string | undefined, defaultValue: number = 0) => {
if (!env) {
return defaultValue;
}
const num = Number(env);
if (isNaN(num)) {
return defaultValue;
}
return num;
}
export const getEnvBoolean = (env: string | undefined, defaultValue: boolean) => {
if (!env) {
return defaultValue;
}
return env === 'true' || env === '1';
}
// From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
export const base64Decode = (base64: string): string => {
const binString = atob(base64);

View file

@ -22,6 +22,6 @@
"@/public/*": ["./public/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/env.mjs"],
"exclude": ["node_modules"]
}

View file

@ -3555,6 +3555,18 @@
"@swc/counter" "^0.1.3"
tslib "^2.4.0"
"@t3-oss/env-core@0.12.0", "@t3-oss/env-core@^0.12.0":
version "0.12.0"
resolved "https://registry.npmjs.org/@t3-oss/env-core/-/env-core-0.12.0.tgz#d5b6d92bf07d2f3ccdf59cc428f1faf114350d35"
integrity sha512-lOPj8d9nJJTt81mMuN9GMk8x5veOt7q9m11OSnCBJhwp1QrL/qR+M8Y467ULBSm9SunosryWNbmQQbgoiMgcdw==
"@t3-oss/env-nextjs@^0.12.0":
version "0.12.0"
resolved "https://registry.npmjs.org/@t3-oss/env-nextjs/-/env-nextjs-0.12.0.tgz#a3a89c5d2eca35c96e8ffd5fe97922873a39c7a0"
integrity sha512-rFnvYk1049RnNVUPvY8iQ55AuQh1Rr+qZzQBh3t++RttCGK4COpXGNxS4+45afuQq02lu+QAOy/5955aU8hRKw==
dependencies:
"@t3-oss/env-core" "0.12.0"
"@tanstack/query-core@5.59.0":
version "5.59.0"
resolved "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.0.tgz"
@ -10474,10 +10486,10 @@ yocto-queue@^0.1.0:
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zod@^3.23.8:
version "3.23.8"
resolved "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz"
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
zod@^3.24.2:
version "3.24.2"
resolved "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz#8efa74126287c675e92f46871cfc8d15c34372b3"
integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==
zwitch@^2.0.4:
version "2.0.4"