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 Dockerfile
.dockerignore .dockerignore
node_modules
npm-debug.log npm-debug.log
README.md README.md
.next
!.next/static
!.next/standalone
.git .git
.sourcebot .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 node:20-alpine3.19 AS node-alpine
FROM golang:1.23.4-alpine3.19 AS go-alpine FROM golang:1.23.4-alpine3.19 AS go-alpine
# ----------------------------------
# ------ Build Zoekt ------ # ------ Build Zoekt ------
FROM go-alpine AS zoekt-builder FROM go-alpine AS zoekt-builder
@ -9,6 +22,7 @@ COPY vendor/zoekt/go.mod vendor/zoekt/go.sum ./
RUN go mod download RUN go mod download
COPY vendor/zoekt ./ COPY vendor/zoekt ./
RUN CGO_ENABLED=0 GOOS=linux go build -o /cmd/ ./cmd/... RUN CGO_ENABLED=0 GOOS=linux go build -o /cmd/ ./cmd/...
# -------------------------
# ------ Build shared libraries ------ # ------ Build shared libraries ------
FROM node-alpine AS shared-libs-builder 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/schemas install --frozen-lockfile
RUN yarn workspace @sourcebot/crypto install --frozen-lockfile RUN yarn workspace @sourcebot/crypto install --frozen-lockfile
RUN yarn workspace @sourcebot/error install --frozen-lockfile RUN yarn workspace @sourcebot/error install --frozen-lockfile
# ------------------------------------
# ------ Build Web ------ # ------ Build Web ------
FROM node-alpine AS web-builder 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 RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
@ -43,26 +72,13 @@ RUN yarn config set network-timeout 1200000
RUN yarn workspace @sourcebot/web install --frozen-lockfile RUN yarn workspace @sourcebot/web install --frozen-lockfile
ENV NEXT_TELEMETRY_DISABLED=1 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 RUN yarn workspace @sourcebot/web build
ENV DOCKER_BUILD=0
# ------------------------------
# ------ Build Backend ------ # ------ Build Backend ------
FROM node-alpine AS backend-builder FROM node-alpine AS backend-builder
ENV DOCKER_BUILD=1
WORKDIR /app WORKDIR /app
COPY package.json yarn.lock* ./ 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 COPY --from=shared-libs-builder /app/packages/error ./packages/error
RUN yarn workspace @sourcebot/backend install --frozen-lockfile RUN yarn workspace @sourcebot/backend install --frozen-lockfile
RUN yarn workspace @sourcebot/backend build RUN yarn workspace @sourcebot/backend build
ENV DOCKER_BUILD=0
# ------------------------------
# ------ Runner ------ # ------ Runner ------
FROM node-alpine AS 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 WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
@ -90,14 +118,6 @@ ENV DATABASE_URL="postgresql://postgres@localhost:5432/sourcebot"
ENV REDIS_URL="redis://localhost:6379" ENV REDIS_URL="redis://localhost:6379"
ENV SRC_TENANT_ENFORCEMENT_MODE=strict 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 # Valid values are: debug, info, warn, error
ENV SOURCEBOT_LOG_LEVEL=info ENV SOURCEBOT_LOG_LEVEL=info
@ -106,18 +126,9 @@ ENV SOURCEBOT_LOG_LEVEL=info
# will serve from http(s)://example.com/sb # will serve from http(s)://example.com/sb
ENV DOMAIN_SUB_PATH=/ 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. # Sourcebot collects anonymous usage data using [PostHog](https://posthog.com/). Uncomment this line to disable.
# ENV SOURCEBOT_TELEMETRY_DISABLED=1 # ENV SOURCEBOT_TELEMETRY_DISABLED=1
ENV STRIPE_PUBLISHABLE_KEY=""
# Configure zoekt # Configure zoekt
COPY vendor/zoekt/install-ctags-alpine.sh . COPY vendor/zoekt/install-ctags-alpine.sh .
RUN ./install-ctags-alpine.sh && rm install-ctags-alpine.sh RUN ./install-ctags-alpine.sh && rm install-ctags-alpine.sh
@ -179,3 +190,4 @@ EXPOSE 3000
ENV PORT=3000 ENV PORT=3000
ENV HOSTNAME="0.0.0.0" 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" 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 # Start the database and wait for it to be ready before starting any other service
if [ "$DATABASE_URL" = "postgresql://postgres@localhost:5432/sourcebot" ]; then if [ "$DATABASE_URL" = "postgresql://postgres@localhost:5432/sourcebot" ]; then
su postgres -c "postgres -D $DB_DATA_DIR" & 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/db": "^0.1.0",
"@sourcebot/error": "^0.1.0", "@sourcebot/error": "^0.1.0",
"@sourcebot/schemas": "^0.1.0", "@sourcebot/schemas": "^0.1.0",
"@t3-oss/env-core": "^0.12.0",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"argparse": "^2.0.1", "argparse": "^2.0.1",
"bullmq": "^5.34.10", "bullmq": "^5.34.10",
@ -47,6 +48,7 @@
"prom-client": "^15.1.3", "prom-client": "^15.1.3",
"simple-git": "^3.27.0", "simple-git": "^3.27.0",
"strip-json-comments": "^5.0.1", "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 { createLogger } from './logger.js';
import micromatch from 'micromatch'; import micromatch from 'micromatch';
import { PrismaClient } from '@sourcebot/db'; import { PrismaClient } from '@sourcebot/db';
import { FALLBACK_GITEA_TOKEN } from './environment.js';
import { processPromiseResults, throwIfAnyFailed } from './connectionUtils.js'; import { processPromiseResults, throwIfAnyFailed } from './connectionUtils.js';
import * as Sentry from "@sentry/node"; import * as Sentry from "@sentry/node";
import { env } from './env.js';
const logger = createLogger('Gitea'); const logger = createLogger('Gitea');
export const getGiteaReposFromConfig = async (config: GiteaConnectionConfig, orgId: number, db: PrismaClient) => { export const getGiteaReposFromConfig = async (config: GiteaConnectionConfig, orgId: number, db: PrismaClient) => {
const tokenResult = config.token ? await getTokenFromConfig(config.token, orgId, db) : undefined; 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', { const api = giteaApi(config.url ?? 'https://gitea.com', {
token: token, token: token,

View file

@ -4,10 +4,10 @@ import { createLogger } from "./logger.js";
import { getTokenFromConfig, measure, fetchWithRetry } from "./utils.js"; import { getTokenFromConfig, measure, fetchWithRetry } from "./utils.js";
import micromatch from "micromatch"; import micromatch from "micromatch";
import { PrismaClient } from "@sourcebot/db"; import { PrismaClient } from "@sourcebot/db";
import { FALLBACK_GITHUB_TOKEN } from "./environment.js";
import { BackendException, BackendError } from "@sourcebot/error"; import { BackendException, BackendError } from "@sourcebot/error";
import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js"; import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
import * as Sentry from "@sentry/node"; import * as Sentry from "@sentry/node";
import { env } from "./env.js";
const logger = createLogger("GitHub"); const logger = createLogger("GitHub");
@ -45,7 +45,7 @@ export const getGitHubReposFromConfig = async (config: GithubConnectionConfig, o
const secretKey = tokenResult?.secretKey; const secretKey = tokenResult?.secretKey;
const octokit = new Octokit({ const octokit = new Octokit({
auth: token ?? FALLBACK_GITHUB_TOKEN, auth: token ?? env.FALLBACK_GITHUB_TOKEN,
...(config.url ? { ...(config.url ? {
baseUrl: `${config.url}/api/v3` 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 { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type"
import { getTokenFromConfig, measure, fetchWithRetry } from "./utils.js"; import { getTokenFromConfig, measure, fetchWithRetry } from "./utils.js";
import { PrismaClient } from "@sourcebot/db"; import { PrismaClient } from "@sourcebot/db";
import { FALLBACK_GITLAB_TOKEN } from "./environment.js";
import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js"; import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
import * as Sentry from "@sentry/node"; import * as Sentry from "@sentry/node";
import { env } from "./env.js";
const logger = createLogger("GitLab"); const logger = createLogger("GitLab");
export const GITLAB_CLOUD_HOSTNAME = "gitlab.com"; export const GITLAB_CLOUD_HOSTNAME = "gitlab.com";
export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig, orgId: number, db: PrismaClient) => { export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig, orgId: number, db: PrismaClient) => {
const tokenResult = config.token ? await getTokenFromConfig(config.token, orgId, db) : undefined; 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({ const api = new Gitlab({
...(token ? { ...(token ? {

View file

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

View file

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

View file

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

View file

@ -1,29 +1,29 @@
import { PostHog } from 'posthog-node'; import { PostHog } from 'posthog-node';
import { PosthogEvent, PosthogEventMap } from './posthogEvents.js'; 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; let posthog: PostHog | undefined = undefined;
if (POSTHOG_PAPIK) { if (env.POSTHOG_PAPIK) {
posthog = new PostHog( 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]) { export function captureEvent<E extends PosthogEvent>(event: E, properties: PosthogEventMap[E]) {
if (SOURCEBOT_TELEMETRY_DISABLED) { if (env.SOURCEBOT_TELEMETRY_DISABLED) {
return; return;
} }
posthog?.capture({ posthog?.capture({
distinctId: SOURCEBOT_INSTALL_ID, distinctId: env.SOURCEBOT_INSTALL_ID,
event: event, event: event,
properties: { properties: {
...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 @@
await import("./src/env.mjs");
import { withSentryConfig } from "@sentry/nextjs"; import { withSentryConfig } from "@sentry/nextjs";
import { env } from "./src/env.mjs";
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
output: "standalone", 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 // @see : https://posthog.com/docs/advanced/proxy/nextjs
async rewrites() { async rewrites() {
return [ return [
{ {
source: "/ingest/static/:path*", 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*", source: "/ingest/:path*",
destination: `${process.env.NEXT_PUBLIC_POSTHOG_HOST}/:path*`, destination: `${env.NEXT_PUBLIC_POSTHOG_HOST}/:path*`,
}, },
{ {
source: "/ingest/decide", source: "/ingest/decide",
destination: `${process.env.NEXT_PUBLIC_POSTHOG_HOST}/decide`, destination: `${env.NEXT_PUBLIC_POSTHOG_HOST}/decide`,
}, },
]; ];
}, },
@ -30,16 +38,7 @@ const nextConfig = {
hostname: '**', 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, { export default withSentryConfig(nextConfig, {

View file

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

View file

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

View file

@ -6,7 +6,7 @@ import { CircleXIcon } from "lucide-react";
import { useDomain } from "@/hooks/useDomain"; import { useDomain } from "@/hooks/useDomain";
import { unwrapServiceError } from "@/lib/utils"; import { unwrapServiceError } from "@/lib/utils";
import useCaptureEvent from "@/hooks/useCaptureEvent"; 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 { useQuery } from "@tanstack/react-query";
import { ConnectionSyncStatus, RepoIndexingStatus } from "@sourcebot/db"; import { ConnectionSyncStatus, RepoIndexingStatus } from "@sourcebot/db";
import { getConnections } from "@/actions"; import { getConnections } from "@/actions";
@ -20,14 +20,14 @@ export const ErrorNavIndicator = () => {
queryKey: ['repos', domain], queryKey: ['repos', domain],
queryFn: () => unwrapServiceError(getRepos(domain)), queryFn: () => unwrapServiceError(getRepos(domain)),
select: (data) => data.filter(repo => repo.repoIndexingStatus === RepoIndexingStatus.FAILED), 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({ const { data: connections, isPending: isPendingConnections, isError: isErrorConnections } = useQuery({
queryKey: ['connections', domain], queryKey: ['connections', domain],
queryFn: () => unwrapServiceError(getConnections(domain)), queryFn: () => unwrapServiceError(getConnections(domain)),
select: (data) => data.filter(connection => connection.syncStatus === ConnectionSyncStatus.FAILED), 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) { 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 { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
import useCaptureEvent from "@/hooks/useCaptureEvent"; import useCaptureEvent from "@/hooks/useCaptureEvent";
import { useDomain } from "@/hooks/useDomain"; 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 { unwrapServiceError } from "@/lib/utils";
import { RepoIndexingStatus } from "@prisma/client"; import { RepoIndexingStatus } from "@prisma/client";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
@ -19,7 +19,7 @@ export const ProgressNavIndicator = () => {
queryKey: ['repos', domain], queryKey: ['repos', domain],
queryFn: () => unwrapServiceError(getRepos(domain)), queryFn: () => unwrapServiceError(getRepos(domain)),
select: (data) => data.filter(repo => repo.repoIndexingStatus === RepoIndexingStatus.IN_INDEX_QUEUE || repo.repoIndexingStatus === RepoIndexingStatus.INDEXING), 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) { if (isPending || isError || inProgressRepos.length === 0) {

View file

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

View file

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

View file

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

View file

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

View file

@ -11,7 +11,7 @@ import { Search, Loader2 } from "lucide-react";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { RepoListItemSkeleton } from "./repoListItemSkeleton"; 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 { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { MultiSelect } from "@/components/ui/multi-select"; 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(); 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({ 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 { getConnections } from "@/actions";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { useQuery } from "@tanstack/react-query"; 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 { RepoIndexingStatus, ConnectionSyncStatus } from "@sourcebot/db";
import { Search } from "lucide-react"; import { Search } from "lucide-react";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@ -43,7 +43,7 @@ export const ConnectionList = ({
const { data: unfilteredConnections, isPending, error } = useQuery({ const { data: unfilteredConnections, isPending, error } = useQuery({
queryKey: ['connections', domain], queryKey: ['connections', domain],
queryFn: () => unwrapServiceError(getConnections(domain)), queryFn: () => unwrapServiceError(getConnections(domain)),
refetchInterval: NEXT_PUBLIC_POLLING_INTERVAL_MS, refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
}); });
const connections = useMemo(() => { const connections = useMemo(() => {

View file

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

View file

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

View file

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

View file

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

View file

@ -1,13 +1,11 @@
'use client'; 'use client';
import { NEXT_PUBLIC_DOMAIN_SUB_PATH } from "@/lib/environment.client";
import { fileSourceResponseSchema, getVersionResponseSchema, listRepositoriesResponseSchema, searchResponseSchema } from "@/lib/schemas"; import { fileSourceResponseSchema, getVersionResponseSchema, listRepositoriesResponseSchema, searchResponseSchema } from "@/lib/schemas";
import { FileSourceRequest, FileSourceResponse, GetVersionResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "@/lib/types"; import { FileSourceRequest, FileSourceResponse, GetVersionResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "@/lib/types";
import assert from "assert"; import assert from "assert";
export const search = async (body: SearchRequest, domain: string): Promise<SearchResponse> => { export const search = async (body: SearchRequest, domain: string): Promise<SearchResponse> => {
const path = resolveServerPath("/api/search"); const result = await fetch("/api/search", {
const result = await fetch(path, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "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> => { export const fetchFileSource = async (body: FileSourceRequest, domain: string): Promise<FileSourceResponse> => {
const path = resolveServerPath("/api/source"); const result = await fetch("/api/source", {
const result = await fetch(path, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -34,8 +31,7 @@ export const fetchFileSource = async (body: FileSourceRequest, domain: string):
} }
export const getRepos = async (domain: string): Promise<ListRepositoriesResponse> => { export const getRepos = async (domain: string): Promise<ListRepositoriesResponse> => {
const path = resolveServerPath("/api/repos"); const result = await fetch("/api/repos", {
const result = await fetch(path, {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -47,8 +43,7 @@ export const getRepos = async (domain: string): Promise<ListRepositoriesResponse
} }
export const getVersion = async (): Promise<GetVersionResponse> => { export const getVersion = async (): Promise<GetVersionResponse> => {
const path = resolveServerPath("/api/version"); const result = await fetch("/api/version", {
const result = await fetch(path, {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -56,13 +51,3 @@ export const getVersion = async (): Promise<GetVersionResponse> => {
}).then(response => response.json()); }).then(response => response.json());
return getVersionResponseSchema.parse(result); 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 { NextRequest } from 'next/server';
import Stripe from 'stripe'; import Stripe from 'stripe';
import { prisma } from '@/prisma'; import { prisma } from '@/prisma';
import { STRIPE_WEBHOOK_SECRET } from '@/lib/environment';
import { getStripe } from '@/lib/stripe';
import { ConnectionSyncStatus, StripeSubscriptionStatus } from '@sourcebot/db'; import { ConnectionSyncStatus, StripeSubscriptionStatus } from '@sourcebot/db';
import { stripeClient } from '@/lib/stripe';
import { env } from '@/env.mjs';
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
const body = await req.text(); const body = await req.text();
@ -14,12 +14,19 @@ export async function POST(req: NextRequest) {
return new Response('No signature', { status: 400 }); 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 { try {
const stripe = getStripe(); const event = stripeClient.webhooks.constructEvent(
const event = stripe.webhooks.constructEvent(
body, body,
signature, signature,
STRIPE_WEBHOOK_SECRET! env.STRIPE_WEBHOOK_SECRET
); );
if (event.type === 'customer.subscription.deleted') { 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"; import { GetVersionResponse } from "@/lib/types";
// Note: In Next.JS 14, GET methods with no params are cached by default at build time. // 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 () => { export const GET = async () => {
return Response.json({ return Response.json({
version: SOURCEBOT_VERSION, version: env.NEXT_PUBLIC_SOURCEBOT_VERSION,
} satisfies GetVersionResponse); } satisfies GetVersionResponse);
} }

View file

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

View file

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

View file

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

View file

@ -6,17 +6,7 @@ import Credentials from "next-auth/providers/credentials"
import EmailProvider from "next-auth/providers/nodemailer"; import EmailProvider from "next-auth/providers/nodemailer";
import { PrismaAdapter } from "@auth/prisma-adapter" import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/prisma"; import { prisma } from "@/prisma";
import { import { env } from "@/env.mjs";
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 { User } from '@sourcebot/db'; import { User } from '@sourcebot/db';
import 'next-auth/jwt'; import 'next-auth/jwt';
import type { Provider } from "next-auth/providers"; import type { Provider } from "next-auth/providers";
@ -44,24 +34,24 @@ declare module 'next-auth/jwt' {
export const getProviders = () => { export const getProviders = () => {
const providers: Provider[] = []; 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({ providers.push(GitHub({
clientId: AUTH_GITHUB_CLIENT_ID, clientId: env.AUTH_GITHUB_CLIENT_ID,
clientSecret: AUTH_GITHUB_CLIENT_SECRET, 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({ providers.push(Google({
clientId: AUTH_GOOGLE_CLIENT_ID, clientId: env.AUTH_GOOGLE_CLIENT_ID,
clientSecret: AUTH_GOOGLE_CLIENT_SECRET, clientSecret: env.AUTH_GOOGLE_CLIENT_SECRET,
})); }));
} }
if (SMTP_CONNECTION_URL && EMAIL_FROM) { if (env.SMTP_CONNECTION_URL && env.EMAIL_FROM) {
providers.push(EmailProvider({ providers.push(EmailProvider({
server: SMTP_CONNECTION_URL, server: env.SMTP_CONNECTION_URL,
from: EMAIL_FROM, from: env.EMAIL_FROM,
maxAge: 60 * 10, maxAge: 60 * 10,
generateVerificationToken: async () => { generateVerificationToken: async () => {
const token = String(Math.floor(100000 + Math.random() * 900000)); const token = String(Math.floor(100000 + Math.random() * 900000));
@ -69,7 +59,7 @@ export const getProviders = () => {
}, },
sendVerificationRequest: async ({ identifier, provider, token }) => { sendVerificationRequest: async ({ identifier, provider, token }) => {
const transport = createTransport(provider.server); 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({ const result = await transport.sendMail({
to: identifier, to: identifier,
from: provider.from, from: provider.from,
@ -86,7 +76,7 @@ export const getProviders = () => {
})); }));
} }
if (AUTH_CREDENTIALS_LOGIN_ENABLED) { if (env.AUTH_CREDENTIALS_LOGIN_ENABLED) {
providers.push(Credentials({ providers.push(Credentials({
credentials: { credentials: {
email: {}, email: {},
@ -102,7 +92,7 @@ export const getProviders = () => {
// authorize runs in the edge runtime (where we cannot make DB calls / access environment variables), // 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. // 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', method: 'POST',
body: JSON.stringify({ email, password }), body: JSON.stringify({ email, password }),
}); });
@ -125,11 +115,11 @@ export const getProviders = () => {
return providers; return providers;
} }
const useSecureCookies = AUTH_URL?.startsWith("https://") ?? false; const useSecureCookies = env.AUTH_URL?.startsWith("https://") ?? false;
const hostName = AUTH_URL ? new URL(AUTH_URL).hostname : "localhost"; const hostName = env.AUTH_URL ? new URL(env.AUTH_URL).hostname : "localhost";
export const { handlers, signIn, signOut, auth } = NextAuth({ export const { handlers, signIn, signOut, auth } = NextAuth({
secret: AUTH_SECRET, secret: env.AUTH_SECRET,
adapter: PrismaAdapter(prisma), adapter: PrismaAdapter(prisma),
session: { session: {
strategy: "jwt", 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 { CaptureOptions } from "posthog-js";
import posthog from "posthog-js"; import posthog from "posthog-js";
import { PosthogEvent, PosthogEventMap } from "../lib/posthogEvents"; 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) { export function captureEvent<E extends PosthogEvent>(event: E, properties: PosthogEventMap[E], options?: CaptureOptions) {
if(!options) { if(!options) {
@ -12,7 +12,7 @@ export function captureEvent<E extends PosthogEvent>(event: E, properties: Posth
options.send_instantly = true; options.send_instantly = true;
posthog.capture(event, { posthog.capture(event, {
...properties, ...properties,
sourcebot_version: NEXT_PUBLIC_SOURCEBOT_VERSION, sourcebot_version: env.NEXT_PUBLIC_SOURCEBOT_VERSION,
}, options); }, 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', STRIPE_CHECKOUT_ERROR = 'STRIPE_CHECKOUT_ERROR',
SECRET_ALREADY_EXISTS = 'SECRET_ALREADY_EXISTS', SECRET_ALREADY_EXISTS = 'SECRET_ALREADY_EXISTS',
SUBSCRIPTION_ALREADY_EXISTS = 'SUBSCRIPTION_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 = { export type PosthogEventMap = {
search_finished: { search_finished: {
query: string | null,
contentBytesLoaded: number, contentBytesLoaded: number,
indexBytesLoaded: number, indexBytesLoaded: number,
crashes: number, crashes: number,

View file

@ -1,5 +1,5 @@
import escapeStringRegexp from "escape-string-regexp"; 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 { listRepositoriesResponseSchema, zoektSearchResponseSchema } from "../schemas";
import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "../types"; import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "../types";
import { fileNotFound, invalidZoektResponse, ServiceError, unexpectedError } from "../serviceError"; import { fileNotFound, invalidZoektResponse, ServiceError, unexpectedError } from "../serviceError";
@ -59,8 +59,8 @@ export const search = async ({ query, maxMatchDisplayCount, whole}: SearchReques
ChunkMatches: true, ChunkMatches: true,
MaxMatchDisplayCount: maxMatchDisplayCount, MaxMatchDisplayCount: maxMatchDisplayCount,
Whole: !!whole, Whole: !!whole,
ShardMaxMatchCount: SHARD_MAX_MATCH_COUNT, ShardMaxMatchCount: env.SHARD_MAX_MATCH_COUNT,
TotalMaxMatchCount: TOTAL_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 { interface ZoektRequest {
path: string, path: string,
@ -17,7 +16,7 @@ export const zoektFetch = async ({
cache, cache,
}: ZoektRequest) => { }: ZoektRequest) => {
const response = await fetch( const response = await fetch(
new URL(path, ZOEKT_WEBSERVER_URL), new URL(path, env.ZOEKT_WEBSERVER_URL),
{ {
method, method,
headers: { headers: {

View file

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

View file

@ -158,30 +158,6 @@ export const isServiceError = (data: unknown): data is ServiceError => {
'message' in data; '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 // From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
export const base64Decode = (base64: string): string => { export const base64Decode = (base64: string): string => {
const binString = atob(base64); const binString = atob(base64);

View file

@ -22,6 +22,6 @@
"@/public/*": ["./public/*"] "@/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"] "exclude": ["node_modules"]
} }

View file

@ -3555,6 +3555,18 @@
"@swc/counter" "^0.1.3" "@swc/counter" "^0.1.3"
tslib "^2.4.0" 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": "@tanstack/query-core@5.59.0":
version "5.59.0" version "5.59.0"
resolved "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.0.tgz" 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" resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zod@^3.23.8: zod@^3.24.2:
version "3.23.8" version "3.24.2"
resolved "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz" resolved "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz#8efa74126287c675e92f46871cfc8d15c34372b3"
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==
zwitch@^2.0.4: zwitch@^2.0.4:
version "2.0.4" version "2.0.4"