feat: platform AI names behind Sign in with OTM (v0.3.0)
- OTM SSO: /api/auth/otm-sso verifies the control-plane ticket (vendored verifier), in-memory jti claim, mints an HMAC cookie (src/lib/session.ts); /api/auth/session + /api/auth/signout. No accounts, no DB. - /api/ai-names: server-side call to the platform's metered gateway (ANTHROPIC_BASE_URL + per-app gateway token), 401 without a session, ~20 req/h per OTM account. Prompt/parser shared with the BYO-key path via src/lib/ai-prompt.ts. - Finder: 'Sign in with OTM for AI names' link → 'AI names' toggle when signed in; own key still overrides. Pairs with platform 0.116.0 (needsAnthropic + needsAuthSecret on names). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XStQKxPfEjrTvWo83KFCxG
This commit is contained in:
@@ -5,6 +5,8 @@ import { generateNames, scoreName, type Idea, type Style } from "@/lib/generate"
|
||||
import { DEFAULT_TLDS, TLDS, UNVERIFIABLE_TLDS } from "@/lib/tlds";
|
||||
import { DEFAULT_REGISTRAR, REGISTRARS, registerUrl } from "@/lib/registrars";
|
||||
import { generateWithAi, loadAiSettings, type AiSettings } from "@/lib/ai";
|
||||
import { generateWithPlatformAi } from "@/lib/platform-ai";
|
||||
import { otmSsoUrl } from "@/lib/otm";
|
||||
import { loadWishlist, saveWishlist, wishlistAsText, WISHLIST_MAX, type SavedName } from "@/lib/wishlist";
|
||||
|
||||
type Availability = "available" | "taken" | "unverified-available" | "unknown";
|
||||
@@ -45,6 +47,12 @@ export default function Finder() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [ai, setAi] = useState<AiSettings | null>(null);
|
||||
const [useAi, setUseAi] = useState(false);
|
||||
// Platform AI: who's signed in with OTM (null = nobody) and whether this
|
||||
// deployment has the gateway wired at all. Fetched once on mount.
|
||||
const [account, setAccount] = useState<{ email: string; name: string | null } | null>(null);
|
||||
const [platformAi, setPlatformAi] = useState(false);
|
||||
const [usePlatformAi, setUsePlatformAi] = useState(true);
|
||||
const [signinNote, setSigninNote] = useState<string | null>(null);
|
||||
const [saved, setSaved] = useState<SavedName[]>([]);
|
||||
const [savedLoaded, setSavedLoaded] = useState(false);
|
||||
const [rechecking, setRechecking] = useState(false);
|
||||
@@ -159,6 +167,38 @@ export default function Finder() {
|
||||
setAi(loadAiSettings());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
fetch("/api/auth/session", { cache: "no-store" })
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((b: { platformAi?: boolean; user?: { email: string; name: string | null } | null } | null) => {
|
||||
if (!alive || !b) return;
|
||||
setPlatformAi(Boolean(b.platformAi));
|
||||
setAccount(b.user ?? null);
|
||||
})
|
||||
.catch(() => {});
|
||||
// The SSO callback bounces failures back here as ?signin=<reason>.
|
||||
const reason = new URLSearchParams(window.location.search).get("signin");
|
||||
if (reason) {
|
||||
setSigninNote(
|
||||
reason === "link_already_used"
|
||||
? "That sign-in link was already used — try again."
|
||||
: reason === "not_enabled"
|
||||
? "Sign-in isn't enabled on this deployment."
|
||||
: "Sign-in didn't complete — try again.",
|
||||
);
|
||||
window.history.replaceState(null, "", window.location.pathname);
|
||||
}
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const signOut = async () => {
|
||||
await fetch("/api/auth/signout", { method: "POST" }).catch(() => {});
|
||||
setAccount(null);
|
||||
};
|
||||
|
||||
// Abandon in-flight lookups when a new search starts, so a slow batch from
|
||||
// the previous query can't paint stale statuses over the new results.
|
||||
const runId = useRef(0);
|
||||
@@ -242,11 +282,16 @@ export default function Finder() {
|
||||
seed: nextSeed,
|
||||
});
|
||||
|
||||
// The visitor's own AI key, if they saved one and asked us to use it.
|
||||
// A failure here is never fatal — the built-in list is already computed.
|
||||
if (useAi && ai) {
|
||||
// AI names: the visitor's own key if they saved one and asked us to use
|
||||
// it, else the platform gateway if they're signed in with OTM. A failure
|
||||
// here is never fatal — the built-in list is already computed.
|
||||
const wantsOwnKey = useAi && ai;
|
||||
const wantsPlatform = !ai && platformAi && account && usePlatformAi;
|
||||
if (wantsOwnKey || wantsPlatform) {
|
||||
try {
|
||||
const aiNames = await generateWithAi(ai, parsedKeywords, style, 16);
|
||||
const aiNames = wantsOwnKey
|
||||
? await generateWithAi(ai, parsedKeywords, style, 16)
|
||||
: await generateWithPlatformAi(parsedKeywords, style, 16);
|
||||
if (runId.current !== myRun) return;
|
||||
const seen = new Set(list.map((i) => i.name));
|
||||
const extra: Idea[] = aiNames
|
||||
@@ -265,7 +310,7 @@ export default function Finder() {
|
||||
setBusy(false);
|
||||
await lookup(domainsFor(list, tlds), myRun);
|
||||
},
|
||||
[ai, domainsFor, lookup, parsedKeywords, style, tlds, useAi],
|
||||
[account, ai, domainsFor, lookup, parsedKeywords, platformAi, style, tlds, useAi, usePlatformAi],
|
||||
);
|
||||
|
||||
const onGenerate = () => {
|
||||
@@ -370,6 +415,26 @@ export default function Finder() {
|
||||
/>
|
||||
Use my {ai.provider === "anthropic" ? "Claude" : "Gemini"} key
|
||||
</label>
|
||||
) : platformAi && account ? (
|
||||
<label
|
||||
style={{ display: "inline-flex", alignItems: "center", gap: 8, fontSize: 14, color: "var(--muted)" }}
|
||||
title={`Signed in as ${account.email}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={usePlatformAi}
|
||||
onChange={(e) => setUsePlatformAi(e.target.checked)}
|
||||
style={{ width: "auto" }}
|
||||
/>
|
||||
AI names
|
||||
<button type="button" className="linkish" onClick={signOut} title={`Sign out ${account.email}`}>
|
||||
sign out
|
||||
</button>
|
||||
</label>
|
||||
) : platformAi ? (
|
||||
<a className="signin" href={otmSsoUrl("/")} title="Free OTM account · adds AI-generated names">
|
||||
Sign in with OTM for AI names →
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -377,6 +442,7 @@ export default function Finder() {
|
||||
<p className="notice info">Pick at least one extension to check.</p>
|
||||
) : null}
|
||||
|
||||
{signinNote ? <p className="notice warn">{signinNote}</p> : null}
|
||||
{error ? <p className="notice warn">{error}</p> : null}
|
||||
|
||||
{showsUnverifiable && ideas.length > 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user