Add OpenRouter API key setup gating for SAM AI features (#356)
This commit is contained in:
parent
36bd1cd7de
commit
a3e7bffd5b
@ -38,6 +38,10 @@
|
|||||||
# LOOPS_TRANSACTIONAL_VERIFY_EMAIL_ID=replace-with-your-loops-verify-template-id
|
# LOOPS_TRANSACTIONAL_VERIFY_EMAIL_ID=replace-with-your-loops-verify-template-id
|
||||||
# LOOPS_TRANSACTIONAL_RESET_PASSWORD_ID=replace-with-your-loops-reset-template-id
|
# LOOPS_TRANSACTIONAL_RESET_PASSWORD_ID=replace-with-your-loops-reset-template-id
|
||||||
|
|
||||||
|
# Optional in self-hosted modes. Required if you want AI features like SAM,
|
||||||
|
# the in-app SEO agent. Create a key at https://openrouter.ai/settings/keys.
|
||||||
|
# OPENROUTER_API_KEY=replace-with-your-openrouter-api-key
|
||||||
|
|
||||||
# Optional in self-hosted modes. Required if you want Google Search Console
|
# Optional in self-hosted modes. Required if you want Google Search Console
|
||||||
# integration and MCP tools. BETTER_AUTH_SECRET is also required for GSC (it
|
# integration and MCP tools. BETTER_AUTH_SECRET is also required for GSC (it
|
||||||
# encrypts the stored OAuth tokens). See docs/SELF_HOSTING_GOOGLE_SEARCH_CONSOLE.md.
|
# encrypts the stored OAuth tokens). See docs/SELF_HOSTING_GOOGLE_SEARCH_CONSOLE.md.
|
||||||
|
|||||||
@ -138,6 +138,10 @@ Search Console is optional and works in self-hosted deployments using your own
|
|||||||
Google OAuth client. It takes ~10 minutes of one-time setup — see
|
Google OAuth client. It takes ~10 minutes of one-time setup — see
|
||||||
[`docs/SELF_HOSTING_GOOGLE_SEARCH_CONSOLE.md`](./docs/SELF_HOSTING_GOOGLE_SEARCH_CONSOLE.md).
|
[`docs/SELF_HOSTING_GOOGLE_SEARCH_CONSOLE.md`](./docs/SELF_HOSTING_GOOGLE_SEARCH_CONSOLE.md).
|
||||||
|
|
||||||
|
## AI Features (SAM)
|
||||||
|
|
||||||
|
AI features like SAM, the in-app SEO agent, are optional — set the `OPENROUTER_API_KEY` environment variable to enable them (create a key at [openrouter.ai/settings/keys](https://openrouter.ai/settings/keys)).
|
||||||
|
|
||||||
## Self-hosting
|
## Self-hosting
|
||||||
|
|
||||||
OpenSEO supports two self-hosting paths:
|
OpenSEO supports two self-hosting paths:
|
||||||
|
|||||||
@ -7,6 +7,9 @@ import {
|
|||||||
invalidateSamSessions,
|
invalidateSamSessions,
|
||||||
samSessionsQueryOptions,
|
samSessionsQueryOptions,
|
||||||
} from "@/client/features/sam/samQueries";
|
} from "@/client/features/sam/samQueries";
|
||||||
|
import { AccessGateLoadingState } from "@/client/features/access-gate/AccessGate";
|
||||||
|
import { useSamAccess } from "./useSamAccess";
|
||||||
|
import { SamSetupGate } from "./SamSetupGate";
|
||||||
import { SamConversation } from "./SamConversation";
|
import { SamConversation } from "./SamConversation";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -23,6 +26,7 @@ export function SamChat({
|
|||||||
activeSessionId: string | undefined;
|
activeSessionId: string | undefined;
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const access = useSamAccess(projectId);
|
||||||
const sessionsQuery = useQuery(samSessionsQueryOptions(projectId));
|
const sessionsQuery = useQuery(samSessionsQueryOptions(projectId));
|
||||||
const sessions = sessionsQuery.data ?? [];
|
const sessions = sessionsQuery.data ?? [];
|
||||||
|
|
||||||
@ -53,6 +57,27 @@ export function SamChat({
|
|||||||
goToSession(firstSessionId);
|
goToSession(firstSessionId);
|
||||||
}, [activeSessionId, firstSessionId, goToSession]);
|
}, [activeSessionId, firstSessionId, goToSession]);
|
||||||
|
|
||||||
|
// Gate the whole page until OPENROUTER_API_KEY is configured — SAM cannot
|
||||||
|
// answer a single turn without it, so surface setup instructions instead of
|
||||||
|
// letting a chat fail mid-stream.
|
||||||
|
if (access.isLoading || !access.enabled) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-auto px-4 py-4 md:px-6 md:py-6">
|
||||||
|
<div className="mx-auto max-w-3xl">
|
||||||
|
{access.isLoading ? (
|
||||||
|
<AccessGateLoadingState />
|
||||||
|
) : (
|
||||||
|
<SamSetupGate
|
||||||
|
errorMessage={access.errorMessage ?? access.statusErrorMessage}
|
||||||
|
isRefetching={access.isRefetching}
|
||||||
|
onRetry={access.onRetry}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (activeSessionId) {
|
if (activeSessionId) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0">
|
<div className="flex h-full min-h-0">
|
||||||
|
|||||||
43
src/client/features/sam/SamSetupGate.tsx
Normal file
43
src/client/features/sam/SamSetupGate.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { AccessGate } from "@/client/features/access-gate/AccessGate";
|
||||||
|
|
||||||
|
export function SamSetupGate({
|
||||||
|
errorMessage,
|
||||||
|
isRefetching,
|
||||||
|
onRetry,
|
||||||
|
}: {
|
||||||
|
errorMessage: string | null;
|
||||||
|
isRefetching: boolean;
|
||||||
|
onRetry: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<AccessGate
|
||||||
|
title="Enable AI Features"
|
||||||
|
bodyText={
|
||||||
|
<>
|
||||||
|
SAM, OpenSEO's in-app AI agent, needs an OpenRouter API key. Create a
|
||||||
|
key on OpenRouter, set it as the <code>OPENROUTER_API_KEY</code>{" "}
|
||||||
|
environment variable, restart OpenSEO, then confirm here.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
helperText={
|
||||||
|
<>
|
||||||
|
Step-by-step instructions for every deployment are in the{" "}
|
||||||
|
<Link
|
||||||
|
className="underline underline-offset-2 hover:text-base-content/70"
|
||||||
|
to="/help/openrouter-api-key"
|
||||||
|
>
|
||||||
|
OpenRouter API key setup guide
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
buttonLabel="Confirm API Key"
|
||||||
|
externalUrl="https://openrouter.ai/settings/keys"
|
||||||
|
externalLabel="Open OpenRouter Keys"
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
isRefetching={isRefetching}
|
||||||
|
onRetry={onRetry}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
src/client/features/sam/useSamAccess.ts
Normal file
10
src/client/features/sam/useSamAccess.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { useAccessGate } from "@/client/features/access-gate/useAccessGate";
|
||||||
|
import { getSamAccessSetupStatus } from "@/serverFunctions/samAccess";
|
||||||
|
|
||||||
|
export function useSamAccess(projectId: string) {
|
||||||
|
return useAccessGate({
|
||||||
|
queryKey: ["samAccessStatus", projectId],
|
||||||
|
queryFn: () => getSamAccessSetupStatus({ data: { projectId } }),
|
||||||
|
statusErrorFallback: "Could not load AI agent setup status.",
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -31,6 +31,7 @@ import { Route as AuthenticatedOnboardingIndexRouteImport } from './routes/_auth
|
|||||||
import { Route as ApiAutumnSplatRouteImport } from './routes/api/autumn/$'
|
import { Route as ApiAutumnSplatRouteImport } from './routes/api/autumn/$'
|
||||||
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
|
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
|
||||||
import { Route as AuthenticatedOnboardingChatRouteImport } from './routes/_authenticated.onboarding.chat'
|
import { Route as AuthenticatedOnboardingChatRouteImport } from './routes/_authenticated.onboarding.chat'
|
||||||
|
import { Route as AppHelpOpenrouterApiKeyRouteImport } from './routes/_app/help/openrouter-api-key'
|
||||||
import { Route as AppHelpDataforseoApiKeyRouteImport } from './routes/_app/help/dataforseo-api-key'
|
import { Route as AppHelpDataforseoApiKeyRouteImport } from './routes/_app/help/dataforseo-api-key'
|
||||||
import { Route as ProjectPProjectIdRouteRouteImport } from './routes/_project/p/$projectId/route'
|
import { Route as ProjectPProjectIdRouteRouteImport } from './routes/_project/p/$projectId/route'
|
||||||
import { Route as ProjectPProjectIdIndexRouteImport } from './routes/_project/p/$projectId/index'
|
import { Route as ProjectPProjectIdIndexRouteImport } from './routes/_project/p/$projectId/index'
|
||||||
@ -161,6 +162,11 @@ const AuthenticatedOnboardingChatRoute =
|
|||||||
path: '/onboarding/chat',
|
path: '/onboarding/chat',
|
||||||
getParentRoute: () => AuthenticatedRoute,
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AppHelpOpenrouterApiKeyRoute = AppHelpOpenrouterApiKeyRouteImport.update({
|
||||||
|
id: '/help/openrouter-api-key',
|
||||||
|
path: '/help/openrouter-api-key',
|
||||||
|
getParentRoute: () => AppRouteRoute,
|
||||||
|
} as any)
|
||||||
const AppHelpDataforseoApiKeyRoute = AppHelpDataforseoApiKeyRouteImport.update({
|
const AppHelpDataforseoApiKeyRoute = AppHelpDataforseoApiKeyRouteImport.update({
|
||||||
id: '/help/dataforseo-api-key',
|
id: '/help/dataforseo-api-key',
|
||||||
path: '/help/dataforseo-api-key',
|
path: '/help/dataforseo-api-key',
|
||||||
@ -285,6 +291,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/subscribe': typeof AuthenticatedSubscribeRoute
|
'/subscribe': typeof AuthenticatedSubscribeRoute
|
||||||
'/p/$projectId': typeof ProjectPProjectIdRouteRouteWithChildren
|
'/p/$projectId': typeof ProjectPProjectIdRouteRouteWithChildren
|
||||||
'/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
|
'/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
|
||||||
|
'/help/openrouter-api-key': typeof AppHelpOpenrouterApiKeyRoute
|
||||||
'/onboarding/chat': typeof AuthenticatedOnboardingChatRoute
|
'/onboarding/chat': typeof AuthenticatedOnboardingChatRoute
|
||||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
'/api/auth/$': typeof ApiAuthSplatRoute
|
||||||
'/api/autumn/$': typeof ApiAutumnSplatRoute
|
'/api/autumn/$': typeof ApiAutumnSplatRoute
|
||||||
@ -323,6 +330,7 @@ export interface FileRoutesByTo {
|
|||||||
'/oauth-consent': typeof AuthenticatedOauthConsentRoute
|
'/oauth-consent': typeof AuthenticatedOauthConsentRoute
|
||||||
'/subscribe': typeof AuthenticatedSubscribeRoute
|
'/subscribe': typeof AuthenticatedSubscribeRoute
|
||||||
'/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
|
'/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
|
||||||
|
'/help/openrouter-api-key': typeof AppHelpOpenrouterApiKeyRoute
|
||||||
'/onboarding/chat': typeof AuthenticatedOnboardingChatRoute
|
'/onboarding/chat': typeof AuthenticatedOnboardingChatRoute
|
||||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
'/api/auth/$': typeof ApiAuthSplatRoute
|
||||||
'/api/autumn/$': typeof ApiAutumnSplatRoute
|
'/api/autumn/$': typeof ApiAutumnSplatRoute
|
||||||
@ -365,6 +373,7 @@ export interface FileRoutesById {
|
|||||||
'/_app/': typeof AppIndexRoute
|
'/_app/': typeof AppIndexRoute
|
||||||
'/_project/p/$projectId': typeof ProjectPProjectIdRouteRouteWithChildren
|
'/_project/p/$projectId': typeof ProjectPProjectIdRouteRouteWithChildren
|
||||||
'/_app/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
|
'/_app/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
|
||||||
|
'/_app/help/openrouter-api-key': typeof AppHelpOpenrouterApiKeyRoute
|
||||||
'/_authenticated/onboarding/chat': typeof AuthenticatedOnboardingChatRoute
|
'/_authenticated/onboarding/chat': typeof AuthenticatedOnboardingChatRoute
|
||||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
'/api/auth/$': typeof ApiAuthSplatRoute
|
||||||
'/api/autumn/$': typeof ApiAutumnSplatRoute
|
'/api/autumn/$': typeof ApiAutumnSplatRoute
|
||||||
@ -406,6 +415,7 @@ export interface FileRouteTypes {
|
|||||||
| '/subscribe'
|
| '/subscribe'
|
||||||
| '/p/$projectId'
|
| '/p/$projectId'
|
||||||
| '/help/dataforseo-api-key'
|
| '/help/dataforseo-api-key'
|
||||||
|
| '/help/openrouter-api-key'
|
||||||
| '/onboarding/chat'
|
| '/onboarding/chat'
|
||||||
| '/api/auth/$'
|
| '/api/auth/$'
|
||||||
| '/api/autumn/$'
|
| '/api/autumn/$'
|
||||||
@ -444,6 +454,7 @@ export interface FileRouteTypes {
|
|||||||
| '/oauth-consent'
|
| '/oauth-consent'
|
||||||
| '/subscribe'
|
| '/subscribe'
|
||||||
| '/help/dataforseo-api-key'
|
| '/help/dataforseo-api-key'
|
||||||
|
| '/help/openrouter-api-key'
|
||||||
| '/onboarding/chat'
|
| '/onboarding/chat'
|
||||||
| '/api/auth/$'
|
| '/api/auth/$'
|
||||||
| '/api/autumn/$'
|
| '/api/autumn/$'
|
||||||
@ -485,6 +496,7 @@ export interface FileRouteTypes {
|
|||||||
| '/_app/'
|
| '/_app/'
|
||||||
| '/_project/p/$projectId'
|
| '/_project/p/$projectId'
|
||||||
| '/_app/help/dataforseo-api-key'
|
| '/_app/help/dataforseo-api-key'
|
||||||
|
| '/_app/help/openrouter-api-key'
|
||||||
| '/_authenticated/onboarding/chat'
|
| '/_authenticated/onboarding/chat'
|
||||||
| '/api/auth/$'
|
| '/api/auth/$'
|
||||||
| '/api/autumn/$'
|
| '/api/autumn/$'
|
||||||
@ -678,6 +690,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AuthenticatedOnboardingChatRouteImport
|
preLoaderRoute: typeof AuthenticatedOnboardingChatRouteImport
|
||||||
parentRoute: typeof AuthenticatedRoute
|
parentRoute: typeof AuthenticatedRoute
|
||||||
}
|
}
|
||||||
|
'/_app/help/openrouter-api-key': {
|
||||||
|
id: '/_app/help/openrouter-api-key'
|
||||||
|
path: '/help/openrouter-api-key'
|
||||||
|
fullPath: '/help/openrouter-api-key'
|
||||||
|
preLoaderRoute: typeof AppHelpOpenrouterApiKeyRouteImport
|
||||||
|
parentRoute: typeof AppRouteRoute
|
||||||
|
}
|
||||||
'/_app/help/dataforseo-api-key': {
|
'/_app/help/dataforseo-api-key': {
|
||||||
id: '/_app/help/dataforseo-api-key'
|
id: '/_app/help/dataforseo-api-key'
|
||||||
path: '/help/dataforseo-api-key'
|
path: '/help/dataforseo-api-key'
|
||||||
@ -822,6 +841,7 @@ interface AppRouteRouteChildren {
|
|||||||
AppSupportRoute: typeof AppSupportRoute
|
AppSupportRoute: typeof AppSupportRoute
|
||||||
AppIndexRoute: typeof AppIndexRoute
|
AppIndexRoute: typeof AppIndexRoute
|
||||||
AppHelpDataforseoApiKeyRoute: typeof AppHelpDataforseoApiKeyRoute
|
AppHelpDataforseoApiKeyRoute: typeof AppHelpDataforseoApiKeyRoute
|
||||||
|
AppHelpOpenrouterApiKeyRoute: typeof AppHelpOpenrouterApiKeyRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppRouteRouteChildren: AppRouteRouteChildren = {
|
const AppRouteRouteChildren: AppRouteRouteChildren = {
|
||||||
@ -832,6 +852,7 @@ const AppRouteRouteChildren: AppRouteRouteChildren = {
|
|||||||
AppSupportRoute: AppSupportRoute,
|
AppSupportRoute: AppSupportRoute,
|
||||||
AppIndexRoute: AppIndexRoute,
|
AppIndexRoute: AppIndexRoute,
|
||||||
AppHelpDataforseoApiKeyRoute: AppHelpDataforseoApiKeyRoute,
|
AppHelpDataforseoApiKeyRoute: AppHelpDataforseoApiKeyRoute,
|
||||||
|
AppHelpOpenrouterApiKeyRoute: AppHelpOpenrouterApiKeyRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppRouteRouteWithChildren = AppRouteRoute._addFileChildren(
|
const AppRouteRouteWithChildren = AppRouteRoute._addFileChildren(
|
||||||
|
|||||||
106
src/routes/_app/help/openrouter-api-key.tsx
Normal file
106
src/routes/_app/help/openrouter-api-key.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
const OPENROUTER_KEYS_URL = "https://openrouter.ai/settings/keys";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_app/help/openrouter-api-key")({
|
||||||
|
component: OpenrouterApiKeyHelpPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
function OpenrouterApiKeyHelpPage() {
|
||||||
|
return (
|
||||||
|
<div className="px-4 py-4 md:px-6 md:py-6 pb-24 md:pb-8 overflow-auto">
|
||||||
|
<div className="mx-auto max-w-3xl space-y-4">
|
||||||
|
<div className="card bg-base-100 border border-base-300">
|
||||||
|
<div className="card-body gap-3">
|
||||||
|
<h1 className="text-2xl font-semibold">
|
||||||
|
Set up your OpenRouter API key
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-base-content/70">
|
||||||
|
OpenSEO needs the <code>OPENROUTER_API_KEY</code> secret before AI
|
||||||
|
features like SAM, the in-app SEO agent, can run. It is optional —
|
||||||
|
everything else in OpenSEO works without it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card bg-base-100 border border-base-300">
|
||||||
|
<div className="card-body gap-4">
|
||||||
|
<h2 className="card-title text-base">Steps</h2>
|
||||||
|
<ol className="list-decimal pl-5 text-sm space-y-3 text-base-content/80">
|
||||||
|
<li>
|
||||||
|
Create an account at{" "}
|
||||||
|
<a
|
||||||
|
className="link link-primary"
|
||||||
|
href="https://openrouter.ai"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
openrouter.ai
|
||||||
|
</a>{" "}
|
||||||
|
and add credits (pay-as-you-go, like DataForSEO).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Go to{" "}
|
||||||
|
<a
|
||||||
|
className="link link-primary"
|
||||||
|
href={OPENROUTER_KEYS_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
OpenRouter API Keys
|
||||||
|
</a>{" "}
|
||||||
|
and click "Create API Key".
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Save the key as the <code>OPENROUTER_API_KEY</code> secret in
|
||||||
|
your environment:
|
||||||
|
<ul className="list-disc pl-5 mt-2 space-y-1">
|
||||||
|
<li>
|
||||||
|
Docker self-hosting: <code>.env</code>
|
||||||
|
</li>
|
||||||
|
<li>Cloudflare: set it in the Workers UI (see below)</li>
|
||||||
|
<li>
|
||||||
|
Local development: <code>.env.local</code>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>Restart OpenSEO.</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card bg-base-100 border border-base-300">
|
||||||
|
<div className="card-body gap-2 text-sm text-base-content/75">
|
||||||
|
<h2 className="card-title text-base">
|
||||||
|
Cloudflare Workers (Dashboard UI)
|
||||||
|
</h2>
|
||||||
|
<ol className="list-decimal pl-5 space-y-2 text-sm text-base-content/80">
|
||||||
|
<li>
|
||||||
|
In Cloudflare, go to <code>Compute</code> ->{" "}
|
||||||
|
<code>Workers & Pages</code>
|
||||||
|
and open your OpenSEO Worker.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Open <code>Settings</code>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Go to <code>Variables & Secrets</code> and add a new secret
|
||||||
|
named
|
||||||
|
<code className="mx-1">OPENROUTER_API_KEY</code>.
|
||||||
|
</li>
|
||||||
|
<li>Paste your OpenRouter API key and save.</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<div className="divider my-1" />
|
||||||
|
|
||||||
|
<p>Or set the same secret from your terminal with:</p>
|
||||||
|
<pre className="p-3 rounded bg-base-200 border border-base-300 overflow-x-auto text-xs">
|
||||||
|
<code>npx wrangler secret put OPENROUTER_API_KEY</code>
|
||||||
|
</pre>
|
||||||
|
<p>Paste your OpenRouter API key when prompted.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
src/serverFunctions/samAccess.ts
Normal file
35
src/serverFunctions/samAccess.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { createServerFn } from "@tanstack/react-start";
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
getOptionalEnvValue,
|
||||||
|
isHostedServerAuthMode,
|
||||||
|
} from "@/server/lib/runtime-env";
|
||||||
|
import { requireProjectContext } from "@/serverFunctions/middleware";
|
||||||
|
|
||||||
|
const OPENROUTER_KEY_MISSING_MESSAGE =
|
||||||
|
"OPENROUTER_API_KEY is not set for this deployment yet. Add it to your environment, restart OpenSEO, then confirm here.";
|
||||||
|
|
||||||
|
const projectScopedSchema = z.object({ projectId: z.string().min(1) });
|
||||||
|
|
||||||
|
type SamAccessStatus = {
|
||||||
|
enabled: boolean;
|
||||||
|
errorMessage: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gates the in-app AI agent (SAM) on an OpenRouter key being configured, the
|
||||||
|
// same way backlinks/AI-search gate on their DataForSEO subscriptions. Hosted
|
||||||
|
// deployments always have the key provisioned, so only self-hosted is checked.
|
||||||
|
export const getSamAccessSetupStatus = createServerFn({ method: "GET" })
|
||||||
|
.middleware(requireProjectContext)
|
||||||
|
.inputValidator((data: unknown) => projectScopedSchema.parse(data))
|
||||||
|
.handler(async (): Promise<SamAccessStatus> => {
|
||||||
|
if (await isHostedServerAuthMode()) {
|
||||||
|
return { enabled: true, errorMessage: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabled = Boolean(await getOptionalEnvValue("OPENROUTER_API_KEY"));
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
errorMessage: enabled ? null : OPENROUTER_KEY_MISSING_MESSAGE,
|
||||||
|
};
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user