Merge pull request #40 from simplr-sh/feat/safe-envs
Refactor environment variable handling and improve API key management
This commit is contained in:
commit
690fd9fc79
29
.env.example
29
.env.example
@ -1,5 +1,28 @@
|
||||
ANTHROPIC_API_KEY=sk-ant-api****
|
||||
# Strictly Server side Env variables
|
||||
XAI_API_KEY=
|
||||
UPSTASH_REDIS_REST_URL=
|
||||
UPSTASH_REDIS_REST_TOKEN=
|
||||
AVIATION_STACK_API_KEY=
|
||||
SANDBOX_TEMPLATE_ID=
|
||||
TMDB_API_KEY=
|
||||
YT_ENDPOINT=
|
||||
EXA_API_KEY=
|
||||
TRIPADVISOR_API_KEY=
|
||||
BLOB_READ_WRITE_TOKEN=
|
||||
ELEVENLABS_API_KEY=
|
||||
AZURE_TRANSLATOR_LOCATION=
|
||||
AZURE_TRANSLATOR_KEY=
|
||||
AZURE_RESOURCE_NAME=
|
||||
AZURE_API_KEY=
|
||||
MAPBOX_ACCESS_TOKEN=
|
||||
FIRECRAWL_API_KEY=
|
||||
TAVILY_API_KEY=tvly-****
|
||||
GROQ_API_KEY=gsk_****
|
||||
OPENWEATHER_API_KEY=***
|
||||
OPENWEATHER_API_KEY=
|
||||
E2B_API_KEY=e2b_****
|
||||
GOOGLE_MAPS_API_KEY=
|
||||
|
||||
# Client side Env variables
|
||||
NEXT_PUBLIC_POSTHOG_KEY=
|
||||
NEXT_PUBLIC_POSTHOG_HOST=
|
||||
NEXT_PUBLIC_MAPBOX_TOKEN=
|
||||
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=
|
||||
@ -1,9 +1,10 @@
|
||||
// app/actions.ts
|
||||
'use server';
|
||||
|
||||
import { serverEnv } from '@/env/server';
|
||||
import { xai } from '@ai-sdk/xai';
|
||||
import { generateObject } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { xai } from '@ai-sdk/xai';
|
||||
|
||||
export async function suggestQuestions(history: any[]) {
|
||||
'use server';
|
||||
@ -37,7 +38,7 @@ Do not use pronouns like he, she, him, his, her, etc. in the questions as they b
|
||||
};
|
||||
}
|
||||
|
||||
const ELEVENLABS_API_KEY = process.env.ELEVENLABS_API_KEY;
|
||||
const ELEVENLABS_API_KEY = serverEnv.ELEVENLABS_API_KEY;
|
||||
|
||||
export async function generateSpeech(text: string, voice: 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer' = "alloy") {
|
||||
|
||||
|
||||
@ -1,21 +1,21 @@
|
||||
// /app/api/chat/route.ts
|
||||
import { z } from "zod";
|
||||
import { xai } from '@ai-sdk/xai'
|
||||
import Exa from 'exa-js'
|
||||
import {
|
||||
convertToCoreMessages,
|
||||
streamText,
|
||||
tool,
|
||||
smoothStream
|
||||
} from "ai";
|
||||
import { BlobRequestAbortedError, put } from '@vercel/blob';
|
||||
import { getGroupConfig } from "@/app/actions";
|
||||
import { serverEnv } from "@/env/server";
|
||||
import { xai } from '@ai-sdk/xai';
|
||||
import CodeInterpreter from "@e2b/code-interpreter";
|
||||
import FirecrawlApp from '@mendable/firecrawl-js';
|
||||
import { tavily } from '@tavily/core'
|
||||
import { getGroupConfig } from "@/app/actions";
|
||||
import { geolocation, ipAddress } from '@vercel/functions'
|
||||
import { tavily } from '@tavily/core';
|
||||
import { Ratelimit } from "@upstash/ratelimit"; // for deno: see above
|
||||
import { Redis } from "@upstash/redis"; // see below for cloudflare and fastly adapters
|
||||
import { BlobRequestAbortedError, put } from '@vercel/blob';
|
||||
import {
|
||||
convertToCoreMessages,
|
||||
smoothStream,
|
||||
streamText,
|
||||
tool
|
||||
} from "ai";
|
||||
import Exa from 'exa-js';
|
||||
import { z } from "zod";
|
||||
|
||||
// Allow streaming responses up to 60 seconds
|
||||
export const maxDuration = 120;
|
||||
@ -182,7 +182,7 @@ export async function POST(req: Request) {
|
||||
searchDepth: ("basic" | "advanced")[];
|
||||
exclude_domains?: string[];
|
||||
}) => {
|
||||
const apiKey = process.env.TAVILY_API_KEY;
|
||||
const apiKey = serverEnv.TAVILY_API_KEY;
|
||||
const tvly = tavily({ apiKey });
|
||||
const includeImageDescriptions = true;
|
||||
|
||||
@ -258,7 +258,7 @@ export async function POST(req: Request) {
|
||||
}),
|
||||
execute: async ({ query }: { query: string }) => {
|
||||
try {
|
||||
const exa = new Exa(process.env.EXA_API_KEY as string);
|
||||
const exa = new Exa(serverEnv.EXA_API_KEY as string);
|
||||
|
||||
const result = await exa.searchAndContents(
|
||||
query,
|
||||
@ -304,7 +304,7 @@ export async function POST(req: Request) {
|
||||
query: z.string().describe("The search query for movies/TV shows"),
|
||||
}),
|
||||
execute: async ({ query }: { query: string }) => {
|
||||
const TMDB_API_KEY = process.env.TMDB_API_KEY;
|
||||
const TMDB_API_KEY = serverEnv.TMDB_API_KEY;
|
||||
const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
|
||||
|
||||
try {
|
||||
@ -389,7 +389,7 @@ export async function POST(req: Request) {
|
||||
description: "Get trending movies from TMDB",
|
||||
parameters: z.object({}),
|
||||
execute: async () => {
|
||||
const TMDB_API_KEY = process.env.TMDB_API_KEY;
|
||||
const TMDB_API_KEY = serverEnv.TMDB_API_KEY;
|
||||
const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
|
||||
|
||||
try {
|
||||
@ -423,7 +423,7 @@ export async function POST(req: Request) {
|
||||
description: "Get trending TV shows from TMDB",
|
||||
parameters: z.object({}),
|
||||
execute: async () => {
|
||||
const TMDB_API_KEY = process.env.TMDB_API_KEY;
|
||||
const TMDB_API_KEY = serverEnv.TMDB_API_KEY;
|
||||
const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
|
||||
|
||||
try {
|
||||
@ -460,7 +460,7 @@ export async function POST(req: Request) {
|
||||
}),
|
||||
execute: async ({ query }: { query: string }) => {
|
||||
try {
|
||||
const exa = new Exa(process.env.EXA_API_KEY as string);
|
||||
const exa = new Exa(serverEnv.EXA_API_KEY as string);
|
||||
|
||||
// Search academic papers with content summary
|
||||
const result = await exa.searchAndContents(
|
||||
@ -516,7 +516,7 @@ export async function POST(req: Request) {
|
||||
}),
|
||||
execute: async ({ query, no_of_results }: { query: string, no_of_results: number }) => {
|
||||
try {
|
||||
const exa = new Exa(process.env.EXA_API_KEY as string);
|
||||
const exa = new Exa(serverEnv.EXA_API_KEY as string);
|
||||
|
||||
// Simple search to get YouTube URLs only
|
||||
const searchResult = await exa.search(
|
||||
@ -545,17 +545,17 @@ export async function POST(req: Request) {
|
||||
try {
|
||||
// Fetch detailed info from our endpoints
|
||||
const [detailsResponse, captionsResponse, timestampsResponse] = await Promise.all([
|
||||
fetch(`${process.env.YT_ENDPOINT}/video-data`, {
|
||||
fetch(`${serverEnv.YT_ENDPOINT}/video-data`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: result.url })
|
||||
}).then(res => res.ok ? res.json() : null),
|
||||
fetch(`${process.env.YT_ENDPOINT}/video-captions`, {
|
||||
fetch(`${serverEnv.YT_ENDPOINT}/video-captions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: result.url })
|
||||
}).then(res => res.ok ? res.text() : null),
|
||||
fetch(`${process.env.YT_ENDPOINT}/video-timestamps`, {
|
||||
fetch(`${serverEnv.YT_ENDPOINT}/video-timestamps`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: result.url })
|
||||
@ -595,7 +595,7 @@ export async function POST(req: Request) {
|
||||
url: z.string().describe("The URL to retrieve the information from."),
|
||||
}),
|
||||
execute: async ({ url }: { url: string }) => {
|
||||
const app = new FirecrawlApp({ apiKey: process.env.FIRECRAWL_API_KEY });
|
||||
const app = new FirecrawlApp({ apiKey: serverEnv.FIRECRAWL_API_KEY });
|
||||
try {
|
||||
const content = await app.scrapeUrl(url);
|
||||
if (!content.success || !content.metadata) {
|
||||
@ -625,7 +625,7 @@ export async function POST(req: Request) {
|
||||
lon: z.number().describe("The longitude of the location."),
|
||||
}),
|
||||
execute: async ({ lat, lon }: { lat: number; lon: number }) => {
|
||||
const apiKey = process.env.OPENWEATHER_API_KEY;
|
||||
const apiKey = serverEnv.OPENWEATHER_API_KEY;
|
||||
const response = await fetch(
|
||||
`https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lon}&appid=${apiKey}`,
|
||||
);
|
||||
@ -645,7 +645,7 @@ export async function POST(req: Request) {
|
||||
console.log("Title:", title);
|
||||
console.log("Icon:", icon);
|
||||
|
||||
const sandbox = await CodeInterpreter.create(process.env.SANDBOX_TEMPLATE_ID!);
|
||||
const sandbox = await CodeInterpreter.create(serverEnv.SANDBOX_TEMPLATE_ID!);
|
||||
const execution = await sandbox.runCode(code);
|
||||
let message = "";
|
||||
let images = [];
|
||||
@ -729,14 +729,14 @@ export async function POST(req: Request) {
|
||||
execute: async ({ query, coordinates }: { query: string; coordinates: number[] }) => {
|
||||
try {
|
||||
// Forward geocoding with Google Maps API
|
||||
const googleApiKey = process.env.GOOGLE_MAPS_API_KEY;
|
||||
const googleApiKey = serverEnv.GOOGLE_MAPS_API_KEY;
|
||||
const googleResponse = await fetch(
|
||||
`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(query)}&key=${googleApiKey}`
|
||||
);
|
||||
const googleData = await googleResponse.json();
|
||||
|
||||
// Reverse geocoding with Mapbox
|
||||
const mapboxToken = process.env.MAPBOX_ACCESS_TOKEN;
|
||||
const mapboxToken = serverEnv.MAPBOX_ACCESS_TOKEN;
|
||||
const [lat, lng] = coordinates;
|
||||
const mapboxResponse = await fetch(
|
||||
`https://api.mapbox.com/search/geocode/v6/reverse?longitude=${lng}&latitude=${lat}&access_token=${mapboxToken}`
|
||||
@ -805,7 +805,7 @@ export async function POST(req: Request) {
|
||||
location?: string;
|
||||
radius?: number;
|
||||
}) => {
|
||||
const mapboxToken = process.env.MAPBOX_ACCESS_TOKEN;
|
||||
const mapboxToken = serverEnv.MAPBOX_ACCESS_TOKEN;
|
||||
|
||||
let proximity = '';
|
||||
if (location) {
|
||||
@ -854,9 +854,9 @@ export async function POST(req: Request) {
|
||||
from: z.string().describe("The source language (optional, will be auto-detected if not provided)."),
|
||||
}),
|
||||
execute: async ({ text, to, from }: { text: string; to: string; from?: string }) => {
|
||||
const key = process.env.AZURE_TRANSLATOR_KEY;
|
||||
const key = serverEnv.AZURE_TRANSLATOR_KEY;
|
||||
const endpoint = "https://api.cognitive.microsofttranslator.com";
|
||||
const location = process.env.AZURE_TRANSLATOR_LOCATION;
|
||||
const location = serverEnv.AZURE_TRANSLATOR_LOCATION;
|
||||
|
||||
const url = `${endpoint}/translate?api-version=3.0&to=${to}${from ? `&from=${from}` : ''}`;
|
||||
|
||||
@ -893,14 +893,14 @@ export async function POST(req: Request) {
|
||||
type: string;
|
||||
radius: number;
|
||||
}) => {
|
||||
const apiKey = process.env.TRIPADVISOR_API_KEY;
|
||||
const apiKey = serverEnv.TRIPADVISOR_API_KEY;
|
||||
let finalLat = latitude;
|
||||
let finalLng = longitude;
|
||||
|
||||
try {
|
||||
// Try geocoding first
|
||||
const geocodingData = await fetch(
|
||||
`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(location)}&key=${process.env.GOOGLE_MAPS_API_KEY}`
|
||||
`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(location)}&key=${serverEnv.GOOGLE_MAPS_API_KEY}`
|
||||
);
|
||||
|
||||
const geocoding = await geocodingData.json();
|
||||
@ -1007,7 +1007,7 @@ export async function POST(req: Request) {
|
||||
|
||||
// Get timezone for the location
|
||||
const tzResponse = await fetch(
|
||||
`https://maps.googleapis.com/maps/api/timezone/json?location=${details.latitude},${details.longitude}×tamp=${Math.floor(Date.now() / 1000)}&key=${process.env.GOOGLE_MAPS_API_KEY}`
|
||||
`https://maps.googleapis.com/maps/api/timezone/json?location=${details.latitude},${details.longitude}×tamp=${Math.floor(Date.now() / 1000)}&key=${serverEnv.GOOGLE_MAPS_API_KEY}`
|
||||
);
|
||||
const tzData = await tzResponse.json();
|
||||
const timezone = tzData.timeZoneId || 'UTC';
|
||||
@ -1131,7 +1131,7 @@ export async function POST(req: Request) {
|
||||
execute: async ({ flight_number }: { flight_number: string }) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://api.aviationstack.com/v1/flights?access_key=${process.env.AVIATION_STACK_API_KEY}&flight_iata=${flight_number}`
|
||||
`https://api.aviationstack.com/v1/flights?access_key=${serverEnv.AVIATION_STACK_API_KEY}&flight_iata=${flight_number}`
|
||||
);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { list, del, ListBlobResult } from '@vercel/blob';
|
||||
import { del, list, ListBlobResult } from '@vercel/blob';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
if (req.headers.get('Authorization') !== `Bearer ${process.env.CRON_SECRET}`) {
|
||||
if (req.headers.get('Authorization') !== `Bearer ${serverEnv.CRON_SECRET}`) {
|
||||
return new NextResponse('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider } from "next-themes"
|
||||
import { ReactNode } from "react"
|
||||
import posthog from 'posthog-js'
|
||||
import { PostHogProvider } from 'posthog-js/react'
|
||||
import { clientEnv } from "@/env/client";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import posthog from 'posthog-js';
|
||||
import { PostHogProvider } from 'posthog-js/react';
|
||||
import { ReactNode } from "react";
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
|
||||
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
posthog.init(clientEnv.NEXT_PUBLIC_POSTHOG_KEY!, {
|
||||
api_host: clientEnv.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
person_profiles: 'always',
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import React, { useEffect, useRef, useCallback } from 'react';
|
||||
import { clientEnv } from "@/env/client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import { cn } from "@/lib/utils";
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
interface Location {
|
||||
lat: number;
|
||||
@ -38,7 +39,7 @@ interface Place {
|
||||
timezone?: string;
|
||||
}
|
||||
|
||||
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN || '';
|
||||
mapboxgl.accessToken = clientEnv.NEXT_PUBLIC_MAPBOX_TOKEN || '';
|
||||
|
||||
interface InteractiveMapProps {
|
||||
center: Location;
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
// /app/components/map-components.tsx
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { clientEnv } from "@/env/client";
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN || '';
|
||||
mapboxgl.accessToken = clientEnv.NEXT_PUBLIC_MAPBOX_TOKEN || '';
|
||||
|
||||
interface Location {
|
||||
lat: number;
|
||||
@ -171,4 +172,4 @@ const MapContainer: React.FC<MapContainerProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export { MapComponent, MapSkeleton, MapContainer };
|
||||
export { MapComponent, MapContainer, MapSkeleton };
|
||||
|
||||
19
env/client.ts
vendored
Normal file
19
env/client.ts
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
// https://env.t3.gg/docs/nextjs#create-your-schema
|
||||
import { createEnv } from '@t3-oss/env-nextjs'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const clientEnv = createEnv({
|
||||
client: {
|
||||
NEXT_PUBLIC_MAPBOX_TOKEN: z.string().min(1),
|
||||
NEXT_PUBLIC_POSTHOG_KEY: z.string().min(1),
|
||||
NEXT_PUBLIC_POSTHOG_HOST: z.string().min(1).url(),
|
||||
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY: z.string().min(1).url(),
|
||||
},
|
||||
runtimeEnv: {
|
||||
NEXT_PUBLIC_MAPBOX_TOKEN: process.env.NEXT_PUBLIC_MAPBOX_TOKEN,
|
||||
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
|
||||
NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY
|
||||
},
|
||||
})
|
||||
|
||||
30
env/server.ts
vendored
Normal file
30
env/server.ts
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
// https://env.t3.gg/docs/nextjs#create-your-schema
|
||||
import { createEnv } from '@t3-oss/env-nextjs'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const serverEnv = createEnv({
|
||||
server: {
|
||||
XAI_API_KEY: z.string().min(1),
|
||||
UPSTASH_REDIS_REST_URL: z.string().min(1).url(),
|
||||
UPSTASH_REDIS_REST_TOKEN: z.string().min(1),
|
||||
ELEVENLABS_API_KEY: z.string().min(1),
|
||||
TAVILY_API_KEY: z.string().min(1),
|
||||
EXA_API_KEY: z.string().min(1),
|
||||
TMDB_API_KEY: z.string().min(1),
|
||||
YT_ENDPOINT: z.string().min(1),
|
||||
FIRECRAWL_API_KEY: z.string().min(1),
|
||||
OPENWEATHER_API_KEY: z.string().min(1),
|
||||
SANDBOX_TEMPLATE_ID: z.string().min(1),
|
||||
GOOGLE_MAPS_API_KEY: z.string().min(1),
|
||||
MAPBOX_ACCESS_TOKEN: z.string().min(1),
|
||||
AZURE_TRANSLATOR_KEY: z.string().min(1),
|
||||
AZURE_TRANSLATOR_LOCATION: z.string().min(1),
|
||||
AZURE_RESOURCE_NAME: z.string().min(1),
|
||||
AZURE_API_KEY: z.string().min(1),
|
||||
TRIPADVISOR_API_KEY: z.string().min(1),
|
||||
AVIATION_STACK_API_KEY: z.string().min(1),
|
||||
CRON_SECRET: z.string().min(1),
|
||||
BLOB_READ_WRITE_TOKEN: z.string().min(1),
|
||||
},
|
||||
experimental__runtimeEnv: process.env,
|
||||
})
|
||||
@ -1,3 +1,12 @@
|
||||
// https://env.t3.gg/docs/nextjs#validate-schema-on-build-(recommended)
|
||||
import { createJiti } from 'jiti'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
const jiti = createJiti(fileURLToPath(import.meta.url))
|
||||
|
||||
// Import env here to validate during build. Using jiti we can import .ts files :)
|
||||
jiti.import('./env/server')
|
||||
jiti.import('./env/client')
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
transpilePackages: ["geist"],
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
"@radix-ui/react-switch": "^1.1.1",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@t3-oss/env-nextjs": "^0.11.1",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@tavily/core": "^0.0.2",
|
||||
"@types/katex": "^0.16.7",
|
||||
@ -54,6 +55,7 @@
|
||||
"geist": "^1.3.1",
|
||||
"google-auth-library": "^9.14.1",
|
||||
"highlight.js": "^11.10.0",
|
||||
"jiti": "^2.4.2",
|
||||
"katex": "^0.16.11",
|
||||
"lucide-react": "^0.424.0",
|
||||
"luxon": "^3.5.0",
|
||||
|
||||
8101
pnpm-lock.yaml
8101
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user