mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-06-22 04:11:55 +10:00
Feat: Add configurable AI provider base URL flag and update documentation (#3059)
* feat: add FLAG_ALLOW_UNSAFE_AI_BASE_URL for configurable AI provider base URLs * feat: add FLAG_ALLOW_UNSAFE_AI_BASE_URL documentation * fix: remove AI_ALLOWED_BASE_URLS from documentation and environment variable reference --------- Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
@@ -89,6 +89,12 @@ FLAG_DISABLE_EMAIL_AUTH="false"
|
||||
# This is useful if you are using a machine with limited resources, like a Raspberry Pi.
|
||||
FLAG_DISABLE_IMAGE_PROCESSING="false"
|
||||
|
||||
# Allows AI providers to be configured with any base URL, including http:// and
|
||||
# private/loopback addresses (e.g. http://localhost:11434 for a local Ollama instance).
|
||||
# WARNING: Enabling this on a multi-tenant deployment is a Server-Side Request Forgery (SSRF)
|
||||
# risk. Only enable this on a trusted, single-tenant self-hosted instance.
|
||||
FLAG_ALLOW_UNSAFE_AI_BASE_URL="false"
|
||||
|
||||
# --- Others ---
|
||||
# Google Cloud API Key (optional)
|
||||
# For font-list generation tooling.
|
||||
|
||||
@@ -104,3 +104,4 @@ Vitest test paths are package-relative when running through `pnpm --filter <pack
|
||||
- Biome uses tabs, double quotes, line width 120, organized import groups, and sorted Tailwind classes for `clsx`, `cva`, and `cn`.
|
||||
- Most packages use `tsgo --noEmit` for typechecking and `vitest run --passWithNoTests` for tests.
|
||||
- There may be unrelated local edits in the worktree. Inspect `git status --short` first and avoid reverting files you did not touch.
|
||||
- **New env vars require a `turbo.json` entry.** Turborepo 2.x runs in strict env mode by default — it filters out env vars that are not listed in `globalEnv` (or task-level `env`/`passThroughEnv`). Any new environment variable added to `packages/env/src/server.ts` must also be added to the `globalEnv` array in `turbo.json`, or the variable will be `undefined` inside child processes at runtime even if it is correctly set in the OS/container environment.
|
||||
|
||||
@@ -76,17 +76,6 @@ const providerOptions: AIProviderOption[] = [
|
||||
},
|
||||
];
|
||||
|
||||
function isValidOptionalBaseURL(value: string) {
|
||||
const trimmedValue = value.trim();
|
||||
if (!trimmedValue) return true;
|
||||
|
||||
try {
|
||||
return new URL(trimmedValue).protocol === "https:";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function AIForm() {
|
||||
const { set, model, apiKey, baseURL, provider, enabled, testStatus } = useAIStore();
|
||||
|
||||
@@ -94,7 +83,7 @@ function AIForm() {
|
||||
return providerOptions.find((option) => option.value === provider);
|
||||
}, [provider]);
|
||||
|
||||
const canTestConnection = model.trim().length > 0 && apiKey.trim().length > 0 && isValidOptionalBaseURL(baseURL);
|
||||
const canTestConnection = model.trim().length > 0 && apiKey.trim().length > 0;
|
||||
|
||||
const { mutate: testConnection, isPending: isTesting } = useMutation(orpc.ai.testConnection.mutationOptions());
|
||||
|
||||
|
||||
@@ -180,7 +180,6 @@ Here's a complete list of environment variables you can configure:
|
||||
| `BETTER_AUTH_API_KEY` | Better Auth dashboard API key | — |
|
||||
| `BETTER_AUTH_URL` | Better Auth base URL override (advanced) | `APP_URL` |
|
||||
| `BETTER_AUTH_SECRET` | Better Auth secret override (advanced) | `AUTH_SECRET` |
|
||||
| `AI_ALLOWED_BASE_URLS` | Allowlist for custom AI provider base URLs | — |
|
||||
| `SMTP_HOST` | SMTP Server Host (for email features) | — |
|
||||
| `SMTP_PORT` | SMTP Server Port | `587` |
|
||||
| `SMTP_USER` | SMTP Username | — |
|
||||
@@ -196,6 +195,7 @@ Here's a complete list of environment variables you can configure:
|
||||
| `FLAG_DISABLE_SIGNUPS` | Disables new user signups | `false` |
|
||||
| `FLAG_DISABLE_EMAIL_AUTH` | Disables email/password login (SSO only) | `false` |
|
||||
| `FLAG_DISABLE_IMAGE_PROCESSING` | Disables image processing | `false` |
|
||||
| `FLAG_ALLOW_UNSAFE_AI_BASE_URL` | Enables local-network AI providers (e.g. Ollama) | `false` |
|
||||
|
||||
> **Note:** Some variables are only required for using related features (OAuth, SMTP, S3, etc.) and can be left unset if unused.
|
||||
|
||||
|
||||
@@ -90,10 +90,6 @@ OAUTH_SCOPES=""
|
||||
# BETTER_AUTH_URL="https://auth.example.com"
|
||||
# BETTER_AUTH_SECRET=""
|
||||
|
||||
# --- AI (optional) ---
|
||||
# Comma-separated hostnames/origins for custom AI base URLs
|
||||
# Example: api.openai.com,https://gateway.ai.vercel.com
|
||||
AI_ALLOWED_BASE_URLS=""
|
||||
|
||||
# --- Email (optional) ---
|
||||
# If all keys are disabled, the app logs the email to be sent to the console instead.
|
||||
@@ -120,6 +116,7 @@ S3_FORCE_PATH_STYLE="false"
|
||||
FLAG_DISABLE_SIGNUPS="false"
|
||||
FLAG_DISABLE_EMAIL_AUTH="false"
|
||||
FLAG_DISABLE_IMAGE_PROCESSING="false"
|
||||
FLAG_ALLOW_UNSAFE_AI_BASE_URL="false"
|
||||
```
|
||||
|
||||
</Step>
|
||||
@@ -254,9 +251,6 @@ docker compose logs -f reactive-resume
|
||||
<li>
|
||||
S3 storage (<code>S3_*</code>)
|
||||
</li>
|
||||
<li>
|
||||
AI URL allowlist (<code>AI_ALLOWED_BASE_URLS</code>)
|
||||
</li>
|
||||
<li>
|
||||
Feature flags (<code>FLAG_*</code>)
|
||||
</li>
|
||||
@@ -337,15 +331,11 @@ openssl rand -hex 32
|
||||
- `"false"` for virtual-hosted-style URLs (`https://bucket.endpoint`) common with AWS S3 / Cloudflare R2.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="AI (optional)">
|
||||
- **`AI_ALLOWED_BASE_URLS`**: Comma-separated hosts or origins allowed as custom AI API base URLs. - Use this when
|
||||
routing AI requests through your own gateway/proxy. - Example: `api.openai.com,https://gateway.ai.vercel.com`
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Feature Flags">
|
||||
<Accordion title="Feature Flags">
|
||||
- **`FLAG_DISABLE_SIGNUPS`**: Disables new signups (web app and server). Useful for private instances.
|
||||
- **`FLAG_DISABLE_EMAIL_AUTH`**: Disables email/password login entirely. Also disables email verification, forgot password, and reset password flows. Users can still sign up via social auth (Google/GitHub/LinkedIn/Custom OAuth), unless FLAG_DISABLE_SIGNUPS is also set to true. Useful when only SSO is required.
|
||||
- **`FLAG_DISABLE_IMAGE_PROCESSING`**: Disables image processing. This is useful if you are using a machine with limited resources, like a Raspberry Pi.
|
||||
- **`FLAG_ALLOW_UNSAFE_AI_BASE_URL`**: Allows AI providers to be configured with `http://` URLs and private/loopback addresses (e.g. a local Ollama instance at `http://192.168.1.10:11434`). **Warning: enabling this on a multi-tenant deployment is an SSRF risk.** Only enable on a trusted, single-tenant self-hosted instance.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
resumePatchProposalToolOutputSchema,
|
||||
} from "@reactive-resume/ai/tools/patch-proposal";
|
||||
import { AI_PROVIDER_DEFAULT_BASE_URLS, aiProviderSchema } from "@reactive-resume/ai/types";
|
||||
import { env } from "@reactive-resume/env/server";
|
||||
import { resumeAnalysisOutputSchema, resumeAnalysisSchema } from "@reactive-resume/schema/resume/analysis";
|
||||
import { applyResumePatches } from "@reactive-resume/utils/resume/patch";
|
||||
import { isPrivateOrLoopbackHost, parseUrl } from "@reactive-resume/utils/url-security.node";
|
||||
@@ -79,9 +80,12 @@ function resolveBaseUrl(input: GetModelInput): string {
|
||||
|
||||
const parsedBaseURL = parseUrl(baseURL);
|
||||
if (!parsedBaseURL) throw new Error("INVALID_AI_BASE_URL");
|
||||
if (parsedBaseURL.protocol !== "https:") throw new Error("INVALID_AI_BASE_URL");
|
||||
if (parsedBaseURL.username || parsedBaseURL.password) throw new Error("INVALID_AI_BASE_URL");
|
||||
if (isPrivateOrLoopbackHost(parsedBaseURL.hostname)) throw new Error("INVALID_AI_BASE_URL");
|
||||
|
||||
if (!env.FLAG_ALLOW_UNSAFE_AI_BASE_URL) {
|
||||
if (parsedBaseURL.protocol !== "https:") throw new Error("INVALID_AI_BASE_URL");
|
||||
if (isPrivateOrLoopbackHost(parsedBaseURL.hostname)) throw new Error("INVALID_AI_BASE_URL");
|
||||
}
|
||||
|
||||
return parsedBaseURL.toString();
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -70,6 +70,7 @@ export const env = createEnv({
|
||||
FLAG_DISABLE_SIGNUPS: z.stringbool().default(false),
|
||||
FLAG_DISABLE_EMAIL_AUTH: z.stringbool().default(false),
|
||||
FLAG_DISABLE_IMAGE_PROCESSING: z.stringbool().default(false),
|
||||
FLAG_ALLOW_UNSAFE_AI_BASE_URL: z.stringbool().default(false),
|
||||
|
||||
// Crowdin (optional, for translation tooling)
|
||||
CROWDIN_PROJECT_ID: z.string().optional(),
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"FLAG_DISABLE_SIGNUPS",
|
||||
"FLAG_DISABLE_EMAIL_AUTH",
|
||||
"FLAG_DISABLE_IMAGE_PROCESSING",
|
||||
"FLAG_ALLOW_UNSAFE_AI_BASE_URL",
|
||||
"GOOGLE_CLOUD_API_KEY",
|
||||
"CROWDIN_PROJECT_ID",
|
||||
"CROWDIN_API_TOKEN"
|
||||
|
||||
Reference in New Issue
Block a user